mirror of
https://github.com/pezkuwichain/revive-differential-tests.git
synced 2026-06-14 12:11:08 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1659164310 | |||
| 0a68800856 | |||
| 8303d789cd | |||
| 40bf44fe58 | |||
| ba8ad03290 | |||
| 3dd99f3ac8 | |||
| 6618463c68 | |||
| dffb80ac0a | |||
| 43a1114337 | |||
| 3a07ea042b | |||
| 9e2aa972db | |||
| 86f2173e8b | |||
| 6e658aec49 | |||
| 1aba74ec3e | |||
| 180bd64bc5 | |||
| 967cbac349 | |||
| a8d84c8360 | |||
| c83a755416 | |||
| 0711216539 | |||
| b40c17c0af | |||
| 8ae994f9de | |||
| 3f3cbfa934 | |||
| c676114fe1 | |||
| 92885351ed | |||
| e16f8ebf59 | |||
| d482808eb2 | |||
| 1f84ce6f61 | |||
| 765569a8b6 | |||
| 6e64f678ee |
@@ -13,4 +13,3 @@ resolc-compiler-tests
|
||||
workdir
|
||||
|
||||
!/schema.json
|
||||
!/dev-genesis.json
|
||||
@@ -0,0 +1,25 @@
|
||||
# Basic
|
||||
edition = "2024"
|
||||
hard_tabs = true
|
||||
max_width = 100
|
||||
use_small_heuristics = "Max"
|
||||
# Imports
|
||||
imports_granularity = "Crate"
|
||||
reorder_imports = true
|
||||
# Consistency
|
||||
newline_style = "Unix"
|
||||
# Misc
|
||||
chain_width = 80
|
||||
spaces_around_ranges = false
|
||||
binop_separator = "Back"
|
||||
reorder_impl_items = false
|
||||
match_arm_leading_pipes = "Preserve"
|
||||
match_arm_blocks = false
|
||||
match_block_trailing_comma = true
|
||||
trailing_comma = "Vertical"
|
||||
trailing_semicolon = false
|
||||
use_field_init_shorthand = true
|
||||
# Format comments
|
||||
comment_width = 100
|
||||
wrap_comments = true
|
||||
|
||||
Generated
+23
-13
@@ -782,15 +782,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
@@ -2337,7 +2328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"syn 2.0.101",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4535,6 +4526,27 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ml-test-runner"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"alloy",
|
||||
"anyhow",
|
||||
"clap",
|
||||
"revive-dt-common",
|
||||
"revive-dt-compiler",
|
||||
"revive-dt-config",
|
||||
"revive-dt-core",
|
||||
"revive-dt-format",
|
||||
"revive-dt-node",
|
||||
"revive-dt-node-interaction",
|
||||
"revive-dt-report",
|
||||
"temp-dir",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moka"
|
||||
version = "0.12.10"
|
||||
@@ -5620,7 +5632,6 @@ name = "revive-dt-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"alloy",
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
"bson",
|
||||
"cacache",
|
||||
@@ -5668,7 +5679,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"alloy",
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
"futures",
|
||||
"revive-common",
|
||||
"revive-dt-common",
|
||||
@@ -5953,7 +5963,7 @@ dependencies = [
|
||||
"security-framework 3.3.0",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs 0.26.11",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -22,9 +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-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" }
|
||||
|
||||
ansi_term = "0.12.1"
|
||||
anyhow = "1.0"
|
||||
async-stream = { version = "0.3.6" }
|
||||
bson = { version = "2.15.0" }
|
||||
cacache = { version = "13.1.0" }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.PHONY: format clippy test machete
|
||||
|
||||
format:
|
||||
cargo fmt --all -- --check
|
||||
cargo +nightly fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
cargo clippy --all-features --workspace -- --deny warnings
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//! This module implements a cached file system allowing for results to be stored in-memory rather
|
||||
//! rather being queried from the file system again.
|
||||
|
||||
use std::fs;
|
||||
use std::io::{Error, Result};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
fs,
|
||||
io::{Error, Result},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use moka::sync::Cache;
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -18,17 +20,14 @@ pub fn read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
|
||||
let content = fs::read(path.as_path())?;
|
||||
READ_CACHE.insert(path, content.clone());
|
||||
Ok(content)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_to_string(path: impl AsRef<Path>) -> Result<String> {
|
||||
let content = read(path)?;
|
||||
String::from_utf8(content).map_err(|_| {
|
||||
Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"The contents of the file are not valid UTF8",
|
||||
)
|
||||
Error::new(std::io::ErrorKind::InvalidData, "The contents of the file are not valid UTF8")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,6 +43,6 @@ pub fn read_dir(path: impl AsRef<Path>) -> Result<Box<dyn Iterator<Item = Result
|
||||
.collect();
|
||||
READ_DIR_CACHE.insert(path.clone(), entries);
|
||||
Ok(read_dir(path).unwrap())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,7 @@ pub fn clear_directory(path: impl AsRef<Path>) -> Result<()> {
|
||||
.with_context(|| format!("Failed to read directory: {}", path.as_ref().display()))?
|
||||
{
|
||||
let entry = entry.with_context(|| {
|
||||
format!(
|
||||
"Failed to read an entry in directory: {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
format!("Failed to read an entry in directory: {}", path.as_ref().display())
|
||||
})?;
|
||||
let entry_path = entry.path();
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::ops::ControlFlow;
|
||||
use std::time::Duration;
|
||||
use std::{ops::ControlFlow, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
|
||||
@@ -38,17 +37,13 @@ where
|
||||
));
|
||||
}
|
||||
|
||||
match future()
|
||||
.await
|
||||
.context("Polled future returned an error during polling loop")?
|
||||
{
|
||||
match future().await.context("Polled future returned an error during polling loop")? {
|
||||
ControlFlow::Continue(()) => {
|
||||
let next_wait_duration = match polling_wait_behavior {
|
||||
PollingWaitBehavior::Constant(duration) => duration,
|
||||
PollingWaitBehavior::ExponentialBackoff => {
|
||||
PollingWaitBehavior::ExponentialBackoff =>
|
||||
Duration::from_secs(2u64.pow(retries))
|
||||
.min(EXPONENTIAL_BACKOFF_MAX_WAIT_DURATION)
|
||||
}
|
||||
.min(EXPONENTIAL_BACKOFF_MAX_WAIT_DURATION),
|
||||
};
|
||||
let next_wait_duration =
|
||||
next_wait_duration.min(max_allowed_wait_duration - total_wait_duration);
|
||||
@@ -56,10 +51,10 @@ where
|
||||
retries += 1;
|
||||
|
||||
tokio::time::sleep(next_wait_duration).await;
|
||||
}
|
||||
},
|
||||
ControlFlow::Break(output) => {
|
||||
break Ok(output);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,13 +75,12 @@ impl Iterator for FilesWithExtensionIterator {
|
||||
for entry_path in iterator.flatten() {
|
||||
if entry_path.is_dir() {
|
||||
self.directories_to_search.push(entry_path)
|
||||
} else if entry_path.is_file()
|
||||
&& entry_path.extension().is_some_and(|ext| {
|
||||
} else if entry_path.is_file() &&
|
||||
entry_path.extension().is_some_and(|ext| {
|
||||
self.allowed_extensions
|
||||
.iter()
|
||||
.any(|allowed| ext.eq_ignore_ascii_case(allowed.as_ref()))
|
||||
})
|
||||
{
|
||||
}) {
|
||||
self.files_matching_allowed_extensions.push(entry_path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,6 @@ macro_rules! define_wrapper_type {
|
||||
};
|
||||
}
|
||||
|
||||
/// Technically not needed but this allows for the macro to be found in the `macros` module of the
|
||||
/// crate in addition to being found in the root of the crate.
|
||||
/// Technically not needed but this allows for the macro to be found in the `macros` module of
|
||||
/// the crate in addition to being found in the root of the crate.
|
||||
pub use {define_wrapper_type, impl_for_wrapper};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use crate::types::VersionOrRequirement;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
use std::{fmt::Display, str::FromStr, sync::LazyLock};
|
||||
|
||||
/// This represents a mode that a given test should be run with, if possible.
|
||||
///
|
||||
@@ -78,9 +76,7 @@ impl FromStr for ModePipeline {
|
||||
// Don't go via Yul IR
|
||||
"E" => Ok(ModePipeline::ViaEVMAssembly),
|
||||
// Anything else that we see isn't a mode at all
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"Unsupported pipeline '{s}': expected 'Y' or 'E'"
|
||||
)),
|
||||
_ => Err(anyhow::anyhow!("Unsupported pipeline '{s}': expected 'Y' or 'E'")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use alloy::primitives::U256;
|
||||
use alloy::signers::local::PrivateKeySigner;
|
||||
use alloy::{primitives::U256, signers::local::PrivateKeySigner};
|
||||
use anyhow::{Context, Result, bail};
|
||||
|
||||
/// This is a sequential private key allocator. When instantiated, it allocated private keys in
|
||||
@@ -16,10 +15,7 @@ pub struct PrivateKeyAllocator {
|
||||
impl PrivateKeyAllocator {
|
||||
/// Creates a new instance of the private key allocator.
|
||||
pub fn new(highest_private_key_inclusive: U256) -> Self {
|
||||
Self {
|
||||
next_private_key: U256::ONE,
|
||||
highest_private_key_inclusive,
|
||||
}
|
||||
Self { next_private_key: U256::ONE, highest_private_key_inclusive }
|
||||
}
|
||||
|
||||
/// Allocates a new private key and errors out if the maximum private key has been reached.
|
||||
|
||||
@@ -7,10 +7,7 @@ pub struct RoundRobinPool<T> {
|
||||
|
||||
impl<T> RoundRobinPool<T> {
|
||||
pub fn new(items: Vec<T>) -> Self {
|
||||
Self {
|
||||
next_index: Default::default(),
|
||||
items,
|
||||
}
|
||||
Self { next_index: Default::default(), items }
|
||||
}
|
||||
|
||||
pub fn round_robin(&self) -> &T {
|
||||
|
||||
@@ -10,8 +10,7 @@ use std::{
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
use alloy::json_abi::JsonAbi;
|
||||
use alloy::primitives::Address;
|
||||
use alloy::{json_abi::JsonAbi, primitives::Address};
|
||||
use anyhow::{Context as _, Result};
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -60,10 +60,7 @@ impl Resolc {
|
||||
Ok(COMPILERS_CACHE
|
||||
.entry(solc.clone())
|
||||
.or_insert_with(|| {
|
||||
Self(Arc::new(ResolcInner {
|
||||
solc,
|
||||
resolc_path: resolc_configuration.path.clone(),
|
||||
}))
|
||||
Self(Arc::new(ResolcInner { solc, resolc_path: resolc_configuration.path.clone() }))
|
||||
})
|
||||
.clone())
|
||||
}
|
||||
@@ -141,9 +138,7 @@ impl SolidityCompiler for Resolc {
|
||||
output_selection: Some(SolcStandardJsonInputSettingsSelection::new_required()),
|
||||
via_ir: Some(true),
|
||||
optimizer: SolcStandardJsonInputSettingsOptimizer::new(
|
||||
optimization
|
||||
.unwrap_or(ModeOptimizerSetting::M0)
|
||||
.optimizations_enabled(),
|
||||
optimization.unwrap_or(ModeOptimizerSetting::M0).optimizations_enabled(),
|
||||
None,
|
||||
&Version::new(0, 0, 0),
|
||||
false,
|
||||
@@ -259,9 +254,8 @@ impl SolidityCompiler for Resolc {
|
||||
.as_ref()
|
||||
.context("No metadata found for the contract")?;
|
||||
let solc_metadata_str = match metadata {
|
||||
serde_json::Value::String(solc_metadata_str) => {
|
||||
solc_metadata_str.as_str()
|
||||
}
|
||||
serde_json::Value::String(solc_metadata_str) =>
|
||||
solc_metadata_str.as_str(),
|
||||
serde_json::Value::Object(metadata_object) => {
|
||||
let solc_metadata_value = metadata_object
|
||||
.get("solc_metadata")
|
||||
@@ -269,18 +263,16 @@ impl SolidityCompiler for Resolc {
|
||||
solc_metadata_value
|
||||
.as_str()
|
||||
.context("The 'solc_metadata' field is not a string")?
|
||||
}
|
||||
serde_json::Value::Null
|
||||
| serde_json::Value::Bool(_)
|
||||
| serde_json::Value::Number(_)
|
||||
| serde_json::Value::Array(_) => {
|
||||
},
|
||||
serde_json::Value::Null |
|
||||
serde_json::Value::Bool(_) |
|
||||
serde_json::Value::Number(_) |
|
||||
serde_json::Value::Array(_) => {
|
||||
anyhow::bail!("Unsupported type of metadata {metadata:?}")
|
||||
}
|
||||
},
|
||||
};
|
||||
let solc_metadata = serde_json::from_str::<serde_json::Value>(
|
||||
solc_metadata_str,
|
||||
)
|
||||
.context(
|
||||
let solc_metadata =
|
||||
serde_json::from_str::<serde_json::Value>(solc_metadata_str).context(
|
||||
"Failed to deserialize the solc_metadata as a serde_json generic value",
|
||||
)?;
|
||||
let output_value = solc_metadata
|
||||
@@ -305,7 +297,7 @@ impl SolidityCompiler for Resolc {
|
||||
optimize_setting: ModeOptimizerSetting,
|
||||
pipeline: ModePipeline,
|
||||
) -> bool {
|
||||
pipeline == ModePipeline::ViaYulIR
|
||||
&& SolidityCompiler::supports_mode(&self.0.solc, optimize_setting, pipeline)
|
||||
pipeline == ModePipeline::ViaYulIR &&
|
||||
SolidityCompiler::supports_mode(&self.0.solc, optimize_setting, pipeline)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,7 @@ use foundry_compilers_artifacts::{
|
||||
output_selection::{
|
||||
BytecodeOutputSelection, ContractOutputSelection, EvmOutputSelection, OutputSelection,
|
||||
},
|
||||
solc::CompilerOutput as SolcOutput,
|
||||
solc::*,
|
||||
solc::{CompilerOutput as SolcOutput, *},
|
||||
};
|
||||
use semver::Version;
|
||||
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
|
||||
@@ -57,9 +56,7 @@ impl Solc {
|
||||
// resolution for us. Therefore, even if the download didn't proceed, this function will
|
||||
// resolve the version requirement into a canonical version of the compiler. It's then up
|
||||
// to us to either use the provided path or not.
|
||||
let version = version
|
||||
.into()
|
||||
.unwrap_or_else(|| solc_configuration.version.clone().into());
|
||||
let version = version.into().unwrap_or_else(|| solc_configuration.version.clone().into());
|
||||
let (version, path) =
|
||||
download_solc(working_directory_configuration.as_path(), version, false)
|
||||
.await
|
||||
@@ -73,10 +70,7 @@ impl Solc {
|
||||
solc_version = %version,
|
||||
"Created a new solc compiler object"
|
||||
);
|
||||
Self(Arc::new(SolcInner {
|
||||
solc_path: path,
|
||||
solc_version: version,
|
||||
}))
|
||||
Self(Arc::new(SolcInner { solc_path: path, solc_version: version }))
|
||||
})
|
||||
.clone())
|
||||
}
|
||||
@@ -253,10 +247,7 @@ impl SolidityCompiler for Solc {
|
||||
let map = compiler_output
|
||||
.contracts
|
||||
.entry(contract_path.canonicalize().with_context(|| {
|
||||
format!(
|
||||
"Failed to canonicalize contract path {}",
|
||||
contract_path.display()
|
||||
)
|
||||
format!("Failed to canonicalize contract path {}", contract_path.display())
|
||||
})?)
|
||||
.or_default();
|
||||
for (contract_name, contract_info) in contracts.into_iter() {
|
||||
@@ -284,10 +275,11 @@ impl SolidityCompiler for Solc {
|
||||
_optimize_setting: ModeOptimizerSetting,
|
||||
pipeline: ModePipeline,
|
||||
) -> bool {
|
||||
// solc 0.8.13 and above supports --via-ir, and less than that does not. Thus, we support mode E
|
||||
// (ie no Yul IR) in either case, but only support Y (via Yul IR) if the compiler is new enough.
|
||||
pipeline == ModePipeline::ViaEVMAssembly
|
||||
|| (pipeline == ModePipeline::ViaYulIR && self.compiler_supports_yul())
|
||||
// solc 0.8.13 and above supports --via-ir, and less than that does not. Thus, we support
|
||||
// mode E (ie no Yul IR) in either case, but only support Y (via Yul IR) if the compiler
|
||||
// is new enough.
|
||||
pipeline == ModePipeline::ViaEVMAssembly ||
|
||||
(pipeline == ModePipeline::ViaYulIR && self.compiler_supports_yul())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,7 @@ pragma solidity >=0.6.9;
|
||||
import "./callable.sol";
|
||||
|
||||
contract Main {
|
||||
function main(
|
||||
uint[1] calldata p1,
|
||||
Callable callable
|
||||
) public pure returns (uint) {
|
||||
function main(uint[1] calldata p1, Callable callable) public returns(uint) {
|
||||
return callable.f(p1);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-80
@@ -154,7 +154,7 @@ impl AsRef<GenesisConfiguration> for Context {
|
||||
Self::Benchmark(..) => {
|
||||
static GENESIS: LazyLock<GenesisConfiguration> = LazyLock::new(Default::default);
|
||||
&GENESIS
|
||||
}
|
||||
},
|
||||
Self::ExportJsonSchema => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -202,23 +202,11 @@ impl AsRef<ReportConfiguration> for Context {
|
||||
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct TestExecutionContext {
|
||||
/// The set of platforms that the differential tests should run on.
|
||||
#[arg(
|
||||
short = 'p',
|
||||
long = "platform",
|
||||
default_values = ["geth-evm-solc", "revive-dev-node-polkavm-resolc"]
|
||||
)]
|
||||
pub platforms: Vec<PlatformIdentifier>,
|
||||
|
||||
/// The output format to use for the tool's output.
|
||||
#[arg(short, long, default_value_t = OutputFormat::CargoTestLike)]
|
||||
pub output_format: OutputFormat,
|
||||
|
||||
/// The working directory that the program will use for all of the temporary artifacts needed at
|
||||
/// runtime.
|
||||
/// 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.
|
||||
/// If not specified, then a temporary directory will be created and used by the program for
|
||||
/// all temporary artifacts.
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
@@ -227,6 +215,14 @@ pub struct TestExecutionContext {
|
||||
)]
|
||||
pub working_directory: WorkingDirectoryConfiguration,
|
||||
|
||||
/// The set of platforms that the differential tests should run on.
|
||||
#[arg(
|
||||
short = 'p',
|
||||
long = "platform",
|
||||
default_values = ["geth-evm-solc", "revive-dev-node-polkavm-resolc"]
|
||||
)]
|
||||
pub platforms: Vec<PlatformIdentifier>,
|
||||
|
||||
/// Configuration parameters for the corpus files to use.
|
||||
#[clap(flatten, next_help_heading = "Corpus Configuration")]
|
||||
pub corpus_configuration: CorpusConfiguration,
|
||||
@@ -286,11 +282,11 @@ pub struct TestExecutionContext {
|
||||
|
||||
#[derive(Clone, Debug, Parser, Serialize)]
|
||||
pub struct BenchmarkingContext {
|
||||
/// The working directory that the program will use for all of the temporary artifacts needed at
|
||||
/// runtime.
|
||||
/// 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.
|
||||
/// If not specified, then a temporary directory will be created and used by the program for
|
||||
/// all temporary artifacts.
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
@@ -584,8 +580,8 @@ pub struct ResolcConfiguration {
|
||||
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.
|
||||
/// 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",
|
||||
@@ -628,13 +624,9 @@ pub struct GethConfiguration {
|
||||
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"
|
||||
)]
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -645,11 +637,7 @@ pub struct KitchensinkConfiguration {
|
||||
///
|
||||
/// If this is not specified, then the tool assumes that it should use the kitchensink binary
|
||||
/// that's provided in the user's $PATH.
|
||||
#[clap(
|
||||
id = "kitchensink.path",
|
||||
long = "kitchensink.path",
|
||||
default_value = "substrate-node"
|
||||
)]
|
||||
#[clap(id = "kitchensink.path", long = "kitchensink.path", default_value = "substrate-node")]
|
||||
pub path: PathBuf,
|
||||
|
||||
/// The amount of time to wait upon startup before considering that the node timed out.
|
||||
@@ -667,8 +655,8 @@ pub struct KitchensinkConfiguration {
|
||||
pub struct ReviveDevNodeConfiguration {
|
||||
/// Specifies the path of the revive dev node to be used by the tool.
|
||||
///
|
||||
/// If this is not specified, then the tool assumes that it should use the revive dev node binary
|
||||
/// that's provided in the user's $PATH.
|
||||
/// If this is not specified, then the tool assumes that it should use the revive dev node
|
||||
/// binary that's provided in the user's $PATH.
|
||||
#[clap(
|
||||
id = "revive-dev-node.path",
|
||||
long = "revive-dev-node.path",
|
||||
@@ -684,14 +672,6 @@ pub struct ReviveDevNodeConfiguration {
|
||||
value_parser = parse_duration
|
||||
)]
|
||||
pub start_timeout_ms: Duration,
|
||||
|
||||
/// The consensus to use for the spawned revive-dev-node.
|
||||
#[clap(
|
||||
id = "revive-dev-node.consensus",
|
||||
long = "revive-dev-node.consensus",
|
||||
default_value = "instant-seal"
|
||||
)]
|
||||
pub consensus: String,
|
||||
}
|
||||
|
||||
/// A set of configuration parameters for the ETH RPC.
|
||||
@@ -743,11 +723,11 @@ impl GenesisConfiguration {
|
||||
Some(genesis_path) => {
|
||||
let genesis_content = read_to_string(genesis_path)?;
|
||||
serde_json::from_str(genesis_content.as_str())?
|
||||
}
|
||||
},
|
||||
None => DEFAULT_GENESIS.clone(),
|
||||
};
|
||||
Ok(self.genesis.get_or_init(|| genesis))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -835,10 +815,7 @@ impl ConcurrencyConfiguration {
|
||||
pub fn concurrency_limit(&self) -> Option<usize> {
|
||||
match self.ignore_concurrency_limit {
|
||||
true => None,
|
||||
false => Some(
|
||||
self.number_concurrent_tasks
|
||||
.unwrap_or(20 * self.number_of_nodes),
|
||||
),
|
||||
false => Some(self.number_concurrent_tasks.unwrap_or(20 * self.number_of_nodes)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -929,9 +906,7 @@ impl Serialize for WorkingDirectoryConfiguration {
|
||||
}
|
||||
|
||||
fn parse_duration(s: &str) -> anyhow::Result<Duration> {
|
||||
u64::from_str(s)
|
||||
.map(Duration::from_millis)
|
||||
.map_err(Into::into)
|
||||
u64::from_str(s).map(Duration::from_millis).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// The Solidity compatible node implementation.
|
||||
@@ -962,29 +937,3 @@ pub enum TestingPlatform {
|
||||
/// A polkadot/Substrate based network
|
||||
Zombienet,
|
||||
}
|
||||
|
||||
/// The output format to use for the test execution output.
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Serialize,
|
||||
ValueEnum,
|
||||
EnumString,
|
||||
Display,
|
||||
AsRefStr,
|
||||
IntoStaticStr,
|
||||
)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum OutputFormat {
|
||||
/// The legacy format that was used in the past for the output.
|
||||
Legacy,
|
||||
|
||||
/// An output format that looks heavily resembles the output from `cargo test`.
|
||||
CargoTestLike,
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ revive-dt-node = { workspace = true }
|
||||
revive-dt-node-interaction = { workspace = true }
|
||||
revive-dt-report = { workspace = true }
|
||||
|
||||
ansi_term = { workspace = true }
|
||||
alloy = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
bson = { workspace = true }
|
||||
|
||||
@@ -22,7 +22,6 @@ use alloy::{
|
||||
},
|
||||
};
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use futures::TryFutureExt;
|
||||
use indexmap::IndexMap;
|
||||
use revive_dt_common::{
|
||||
futures::{PollingWaitBehavior, poll},
|
||||
@@ -36,7 +35,7 @@ use revive_dt_format::{
|
||||
},
|
||||
traits::{ResolutionContext, ResolverApi},
|
||||
};
|
||||
use tokio::sync::{Mutex, OnceCell, mpsc::UnboundedSender};
|
||||
use tokio::sync::{Mutex, mpsc::UnboundedSender};
|
||||
use tracing::{Instrument, Span, debug, error, field::display, info, info_span, instrument};
|
||||
|
||||
use crate::{
|
||||
@@ -60,8 +59,8 @@ pub struct Driver<'a, I> {
|
||||
/// 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.
|
||||
/// 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.
|
||||
@@ -124,7 +123,13 @@ where
|
||||
&self.platform_information.reporter,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|err| error!(?err, "Pre-linking compilation failed"))
|
||||
.inspect_err(|err| {
|
||||
error!(
|
||||
?err,
|
||||
platform_identifier = %self.platform_information.platform.platform_identifier(),
|
||||
"Pre-linking compilation failed"
|
||||
)
|
||||
})
|
||||
.context("Failed to produce the pre-linking compiled contracts")?;
|
||||
|
||||
let mut deployed_libraries = None::<HashMap<_, _>>;
|
||||
@@ -132,7 +137,13 @@ where
|
||||
.test_definition
|
||||
.metadata
|
||||
.contract_sources()
|
||||
.inspect_err(|err| error!(?err, "Failed to retrieve contract sources from metadata"))
|
||||
.inspect_err(|err| {
|
||||
error!(
|
||||
?err,
|
||||
platform_identifier = %self.platform_information.platform.platform_identifier(),
|
||||
"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
|
||||
@@ -180,23 +191,22 @@ where
|
||||
TransactionRequest::default().from(deployer_address),
|
||||
code,
|
||||
);
|
||||
let receipt = self
|
||||
.execute_transaction(tx)
|
||||
.and_then(|(_, receipt_fut)| receipt_fut)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
let receipt = self.execute_transaction(tx).await.inspect_err(|err| {
|
||||
error!(
|
||||
?err,
|
||||
%library_instance,
|
||||
platform_identifier = %self.platform_information.platform.platform_identifier(),
|
||||
"Failed to deploy the library"
|
||||
)
|
||||
})?;
|
||||
|
||||
debug!(?library_instance, "Deployed library");
|
||||
debug!(
|
||||
?library_instance,
|
||||
platform_identifier = %self.platform_information.platform.platform_identifier(),
|
||||
"Deployed library"
|
||||
);
|
||||
|
||||
let library_address = receipt
|
||||
.contract_address
|
||||
.expect("Failed to deploy the library");
|
||||
let library_address = receipt.contract_address.expect("Failed to deploy the library");
|
||||
|
||||
deployed_libraries.get_or_insert_default().insert(
|
||||
library_instance.clone(),
|
||||
@@ -215,13 +225,17 @@ where
|
||||
&self.platform_information.reporter,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|err| error!(?err, "Post-linking compilation failed"))
|
||||
.inspect_err(|err| {
|
||||
error!(
|
||||
?err,
|
||||
platform_identifier = %self.platform_information.platform.platform_identifier(),
|
||||
"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(),
|
||||
);
|
||||
self.execution_state =
|
||||
ExecutionState::new(compiler_output.contracts, deployed_libraries.unwrap_or_default());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -251,6 +265,7 @@ where
|
||||
skip_all,
|
||||
fields(
|
||||
driver_id = self.driver_id,
|
||||
platform_identifier = %self.platform_information.platform.platform_identifier(),
|
||||
%step_path,
|
||||
),
|
||||
err(Debug),
|
||||
@@ -286,11 +301,15 @@ where
|
||||
.handle_function_call_contract_deployment(step)
|
||||
.await
|
||||
.context("Failed to deploy contracts for the function call step")?;
|
||||
let transaction_hash = self
|
||||
let execution_receipt = 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)
|
||||
let tracing_result = self
|
||||
.handle_function_call_call_frame_tracing(execution_receipt.transaction_hash)
|
||||
.await
|
||||
.context("Failed to handle the function call call frame tracing")?;
|
||||
self.handle_function_call_variable_assignment(step, &tracing_result)
|
||||
.await
|
||||
.context("Failed to handle function call variable assignment")?;
|
||||
Ok(1)
|
||||
@@ -302,11 +321,7 @@ where
|
||||
) -> 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)
|
||||
{
|
||||
if !self.execution_state.deployed_contracts.contains_key(&instance) {
|
||||
instances_we_must_deploy.entry(instance).or_insert(false);
|
||||
}
|
||||
}
|
||||
@@ -318,15 +333,11 @@ where
|
||||
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 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?
|
||||
step.caller.resolve_address(self.resolver.as_ref(), context).await?
|
||||
};
|
||||
if let (_, _, Some(receipt)) = self
|
||||
.get_or_deploy_contract_instance(&instance, caller, calldata, value)
|
||||
@@ -344,20 +355,19 @@ where
|
||||
&mut self,
|
||||
step: &FunctionCallStep,
|
||||
mut deployment_receipts: HashMap<ContractInstance, TransactionReceipt>,
|
||||
) -> Result<TxHash> {
|
||||
) -> Result<TransactionReceipt> {
|
||||
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),
|
||||
.context("Failed to find deployment receipt for constructor call"),
|
||||
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)
|
||||
}
|
||||
self.execute_transaction(tx).await
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,29 +405,20 @@ where
|
||||
async fn handle_function_call_variable_assignment(
|
||||
&mut self,
|
||||
step: &FunctionCallStep,
|
||||
tx_hash: TxHash,
|
||||
tracing_result: &CallFrame,
|
||||
) -> 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),
|
||||
) {
|
||||
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.execution_state
|
||||
.variables
|
||||
.insert(variable_name.clone(), value);
|
||||
self.execution_state.variables.insert(variable_name.clone(), value);
|
||||
tracing::info!(
|
||||
variable_name,
|
||||
variable_value = hex::encode(value.to_be_bytes::<32>()),
|
||||
@@ -485,9 +486,7 @@ where
|
||||
// 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,
|
||||
})
|
||||
.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)
|
||||
@@ -515,9 +514,7 @@ where
|
||||
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);
|
||||
self.execution_state.variables.insert(variable_name.to_string(), variable);
|
||||
|
||||
Ok(1)
|
||||
}
|
||||
@@ -529,6 +526,7 @@ where
|
||||
skip_all,
|
||||
fields(
|
||||
driver_id = self.driver_id,
|
||||
platform_identifier = %self.platform_information.platform.platform_identifier(),
|
||||
%contract_instance,
|
||||
%deployer
|
||||
),
|
||||
@@ -541,10 +539,8 @@ where
|
||||
calldata: Option<&Calldata>,
|
||||
value: Option<EtherValue>,
|
||||
) -> Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
|
||||
if let Some((_, address, abi)) = self
|
||||
.execution_state
|
||||
.deployed_contracts
|
||||
.get(contract_instance)
|
||||
if let Some((_, address, abi)) =
|
||||
self.execution_state.deployed_contracts.get(contract_instance)
|
||||
{
|
||||
info!(
|
||||
|
||||
@@ -571,6 +567,7 @@ where
|
||||
skip_all,
|
||||
fields(
|
||||
driver_id = self.driver_id,
|
||||
platform_identifier = %self.platform_information.platform.platform_identifier(),
|
||||
%contract_instance,
|
||||
%deployer
|
||||
),
|
||||
@@ -583,19 +580,10 @@ where
|
||||
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)
|
||||
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
|
||||
)
|
||||
anyhow::bail!("Contract source not found for instance {:?}", contract_instance)
|
||||
};
|
||||
|
||||
let Some((code, abi)) = self
|
||||
@@ -605,10 +593,7 @@ where
|
||||
.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
|
||||
)
|
||||
anyhow::bail!("Failed to find information for contract {:?}", contract_instance)
|
||||
};
|
||||
|
||||
let mut code = match alloy::hex::decode(&code) {
|
||||
@@ -621,7 +606,7 @@ where
|
||||
"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 {
|
||||
@@ -640,16 +625,12 @@ where
|
||||
TransactionBuilder::<Ethereum>::with_deploy_code(tx, code)
|
||||
};
|
||||
|
||||
let receipt = match self
|
||||
.execute_transaction(tx)
|
||||
.and_then(|(_, receipt_fut)| receipt_fut)
|
||||
.await
|
||||
{
|
||||
let receipt = match self.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 {
|
||||
@@ -664,10 +645,9 @@ where
|
||||
.reporter
|
||||
.report_contract_deployed_event(contract_instance.clone(), address)?;
|
||||
|
||||
self.execution_state.deployed_contracts.insert(
|
||||
contract_instance.clone(),
|
||||
(contract_ident, address, abi.clone()),
|
||||
);
|
||||
self.execution_state
|
||||
.deployed_contracts
|
||||
.insert(contract_instance.clone(), (contract_ident, address, abi.clone()));
|
||||
|
||||
Ok((address, abi, receipt))
|
||||
}
|
||||
@@ -680,9 +660,7 @@ where
|
||||
match step_address {
|
||||
StepAddress::Address(address) => Ok(*address),
|
||||
StepAddress::ResolvableAddress(resolvable) => {
|
||||
let Some(instance) = resolvable
|
||||
.strip_suffix(".address")
|
||||
.map(ContractInstance::new)
|
||||
let Some(instance) = resolvable.strip_suffix(".address").map(ContractInstance::new)
|
||||
else {
|
||||
bail!("Not an address variable");
|
||||
};
|
||||
@@ -695,7 +673,7 @@ where
|
||||
)
|
||||
.await
|
||||
.map(|v| v.0)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
// endregion:Contract Deployment
|
||||
@@ -718,7 +696,7 @@ where
|
||||
async fn execute_transaction(
|
||||
&self,
|
||||
transaction: TransactionRequest,
|
||||
) -> anyhow::Result<(TxHash, impl Future<Output = Result<TransactionReceipt>>)> {
|
||||
) -> anyhow::Result<TransactionReceipt> {
|
||||
let node = self.platform_information.node;
|
||||
let transaction_hash = node
|
||||
.submit_transaction(transaction)
|
||||
@@ -731,7 +709,6 @@ where
|
||||
.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),
|
||||
@@ -742,17 +719,14 @@ where
|
||||
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
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 tracing::{error, info, info_span, instrument, warn};
|
||||
|
||||
use revive_dt_config::{BenchmarkingContext, Context};
|
||||
use revive_dt_report::Reporter;
|
||||
@@ -96,13 +96,8 @@ pub async fn handle_differential_benchmarks(
|
||||
// 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,
|
||||
context.working_directory.as_path().join("compilation_cache"),
|
||||
context.compilation_configuration.invalidate_compilation_cache,
|
||||
)
|
||||
.await
|
||||
.map(Arc::new)
|
||||
@@ -159,14 +154,9 @@ pub async fn handle_differential_benchmarks(
|
||||
|
||||
futures::future::try_join(
|
||||
watcher.run(),
|
||||
driver
|
||||
.execute_all()
|
||||
.instrument(info_span!("Executing Benchmarks", %platform_identifier))
|
||||
.inspect(|_| {
|
||||
driver.execute_all().inspect(|_| {
|
||||
info!("All transactions submitted - driver completed execution");
|
||||
watcher_tx
|
||||
.send(WatcherEvent::AllTransactionsSubmitted)
|
||||
.unwrap()
|
||||
watcher_tx.send(WatcherEvent::AllTransactionsSubmitted).unwrap()
|
||||
}),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -10,14 +10,15 @@ 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.
|
||||
/// 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.
|
||||
/// This map stores the variables used for each one of the cases contained in the metadata
|
||||
/// file.
|
||||
pub variables: HashMap<String, U256>,
|
||||
}
|
||||
|
||||
@@ -26,11 +27,7 @@ impl ExecutionState {
|
||||
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
||||
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
compiled_contracts,
|
||||
deployed_contracts,
|
||||
variables: Default::default(),
|
||||
}
|
||||
Self { compiled_contracts, deployed_contracts, variables: Default::default() }
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
|
||||
@@ -33,14 +33,7 @@ impl Watcher {
|
||||
blocks_stream: Pin<Box<dyn Stream<Item = MinedBlockInformation>>>,
|
||||
) -> (Self, UnboundedSender<WatcherEvent>) {
|
||||
let (tx, rx) = unbounded_channel::<WatcherEvent>();
|
||||
(
|
||||
Self {
|
||||
platform_identifier,
|
||||
rx,
|
||||
blocks_stream,
|
||||
},
|
||||
tx,
|
||||
)
|
||||
(Self { platform_identifier, rx, blocks_stream }, tx)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
@@ -49,9 +42,8 @@ impl Watcher {
|
||||
// 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
|
||||
let Some(WatcherEvent::RepetitionStartEvent { ignore_block_before }) =
|
||||
self.rx.recv().await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
@@ -80,19 +72,16 @@ impl Watcher {
|
||||
// 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::RepetitionStartEvent { .. } => {},
|
||||
WatcherEvent::SubmittedTransaction { transaction_hash } => {
|
||||
watch_for_transaction_hashes
|
||||
.write()
|
||||
.await
|
||||
.insert(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;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,10 +93,6 @@ impl Watcher {
|
||||
async move {
|
||||
let mut mined_blocks_information = Vec::new();
|
||||
|
||||
// region:TEMPORARY
|
||||
eprintln!("Watcher information for {}", self.platform_identifier);
|
||||
eprintln!("block_number,block_timestamp,mined_gas,block_gas_limit,tx_count");
|
||||
// endregion:TEMPORARY
|
||||
while let Some(block) = blocks_information_stream.next().await {
|
||||
// If the block number is equal to or less than the last block before the
|
||||
// repetition then we ignore it and continue on to the next block.
|
||||
@@ -115,16 +100,15 @@ impl Watcher {
|
||||
continue;
|
||||
}
|
||||
|
||||
if *all_transactions_submitted.read().await
|
||||
&& watch_for_transaction_hashes.read().await.is_empty()
|
||||
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(),
|
||||
block_tx_count = block.transaction_hashes.len(),
|
||||
"Observed a block"
|
||||
);
|
||||
|
||||
@@ -136,20 +120,6 @@ impl Watcher {
|
||||
watch_for_transaction_hashes.remove(tx_hash);
|
||||
}
|
||||
|
||||
// region:TEMPORARY
|
||||
// TODO: The following core is TEMPORARY and will be removed once we have proper
|
||||
// reporting in place and then it can be removed. This serves as as way of doing
|
||||
// some very simple reporting for the time being.
|
||||
eprintln!(
|
||||
"\"{}\",\"{}\",\"{}\",\"{}\",\"{}\"",
|
||||
block.block_number,
|
||||
block.block_timestamp,
|
||||
block.mined_gas,
|
||||
block.block_gas_limit,
|
||||
block.transaction_hashes.len()
|
||||
);
|
||||
// endregion:TEMPORARY
|
||||
|
||||
mined_blocks_information.push(block);
|
||||
}
|
||||
|
||||
@@ -158,10 +128,34 @@ impl Watcher {
|
||||
}
|
||||
};
|
||||
|
||||
let (_, _) =
|
||||
let (_, mined_blocks_information) =
|
||||
futures::future::join(watcher_event_watching_task, block_information_watching_task)
|
||||
.await;
|
||||
|
||||
// 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.
|
||||
use std::io::Write;
|
||||
|
||||
let mut stderr = std::io::stderr().lock();
|
||||
writeln!(stderr, "Watcher information for {}", self.platform_identifier)?;
|
||||
writeln!(stderr, "block_number,block_timestamp,mined_gas,block_gas_limit,tx_count")?;
|
||||
for block in mined_blocks_information {
|
||||
writeln!(
|
||||
stderr,
|
||||
"{},{},{},{},{}",
|
||||
block.block_number,
|
||||
block.block_timestamp,
|
||||
block.mined_gas,
|
||||
block.block_gas_limit,
|
||||
block.transaction_hashes.len()
|
||||
)?
|
||||
}
|
||||
}
|
||||
// endregion:TEMPORARY
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,9 +112,7 @@ impl<'a> Driver<'a, StepsIterator> {
|
||||
pub async fn execute_all(mut self) -> Result<usize> {
|
||||
let platform_drivers = std::mem::take(&mut self.platform_drivers);
|
||||
let results = futures::future::try_join_all(
|
||||
platform_drivers
|
||||
.into_values()
|
||||
.map(|driver| driver.execute_all()),
|
||||
platform_drivers.into_values().map(|driver| driver.execute_all()),
|
||||
)
|
||||
.await
|
||||
.context("Failed to execute all of the steps on the driver")?;
|
||||
@@ -131,8 +129,8 @@ pub struct PlatformDriver<'a, I> {
|
||||
/// 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.
|
||||
/// 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.
|
||||
@@ -252,11 +250,8 @@ where
|
||||
TransactionRequest::default().from(deployer_address),
|
||||
code,
|
||||
);
|
||||
let receipt = platform_information
|
||||
.node
|
||||
.execute_transaction(tx)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
let receipt =
|
||||
platform_information.node.execute_transaction(tx).await.inspect_err(|err| {
|
||||
error!(
|
||||
?err,
|
||||
%library_instance,
|
||||
@@ -265,9 +260,7 @@ where
|
||||
)
|
||||
})?;
|
||||
|
||||
let library_address = receipt
|
||||
.contract_address
|
||||
.expect("Failed to deploy the library");
|
||||
let library_address = receipt.contract_address.expect("Failed to deploy the library");
|
||||
|
||||
deployed_libraries.get_or_insert_default().insert(
|
||||
library_instance.clone(),
|
||||
@@ -295,10 +288,7 @@ where
|
||||
})
|
||||
.context("Failed to compile the post-link contracts")?;
|
||||
|
||||
Ok(ExecutionState::new(
|
||||
compiler_output.contracts,
|
||||
deployed_libraries.unwrap_or_default(),
|
||||
))
|
||||
Ok(ExecutionState::new(compiler_output.contracts, deployed_libraries.unwrap_or_default()))
|
||||
}
|
||||
// endregion:Constructors & Initialization
|
||||
|
||||
@@ -364,14 +354,31 @@ where
|
||||
_: &StepPath,
|
||||
step: &FunctionCallStep,
|
||||
) -> Result<usize> {
|
||||
// Check if this step expects an exception
|
||||
let expects_exception = step.expected.as_ref().map_or(false, |expected| match expected {
|
||||
Expected::Expected(exp) => exp.exception,
|
||||
Expected::ExpectedMany(exps) => exps.iter().any(|exp| exp.exception),
|
||||
Expected::Calldata(_) => false,
|
||||
});
|
||||
|
||||
let deployment_receipts = self
|
||||
.handle_function_call_contract_deployment(step)
|
||||
.await
|
||||
.context("Failed to deploy contracts for the function call step")?;
|
||||
let execution_receipt = self
|
||||
.handle_function_call_execution(step, deployment_receipts)
|
||||
.await
|
||||
.context("Failed to handle the function call execution")?;
|
||||
|
||||
let execution_receipt =
|
||||
match self.handle_function_call_execution(step, deployment_receipts).await {
|
||||
Ok(receipt) => Some(receipt),
|
||||
Err(err) => {
|
||||
if !expects_exception {
|
||||
return Err(err).context("Failed to handle the function call execution");
|
||||
}
|
||||
tracing::info!("Transaction failed as expected");
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(execution_receipt) = execution_receipt {
|
||||
let tracing_result = self
|
||||
.handle_function_call_call_frame_tracing(execution_receipt.transaction_hash)
|
||||
.await
|
||||
@@ -382,6 +389,8 @@ where
|
||||
self.handle_function_call_assertions(step, &execution_receipt, &tracing_result)
|
||||
.await
|
||||
.context("Failed to handle function call assertions")?;
|
||||
}
|
||||
|
||||
Ok(1)
|
||||
}
|
||||
|
||||
@@ -392,11 +401,7 @@ where
|
||||
) -> 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)
|
||||
{
|
||||
if !self.execution_state.deployed_contracts.contains_key(&instance) {
|
||||
instances_we_must_deploy.entry(instance).or_insert(false);
|
||||
}
|
||||
}
|
||||
@@ -408,16 +413,13 @@ where
|
||||
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 value = deploy_with_constructor_arguments.then_some(step.value).flatten();
|
||||
|
||||
let caller = {
|
||||
let context = self.default_resolution_context();
|
||||
let resolver = self.platform_information.node.resolver().await?;
|
||||
step.caller
|
||||
.resolve_address(resolver.as_ref(), context)
|
||||
.await?
|
||||
let resolved = step.caller.resolve_address(resolver.as_ref(), context).await?;
|
||||
self.platform_information.node.resolve_signer_or_default(resolved)
|
||||
};
|
||||
if let (_, _, Some(receipt)) = self
|
||||
.get_or_deploy_contract_instance(&instance, caller, calldata, value)
|
||||
@@ -445,18 +447,23 @@ where
|
||||
.context("Failed to find deployment receipt for constructor call"),
|
||||
Method::Fallback | Method::FunctionName(_) => {
|
||||
let resolver = self.platform_information.node.resolver().await?;
|
||||
let tx = match step
|
||||
let mut tx = match step
|
||||
.as_transaction(resolver.as_ref(), self.default_resolution_context())
|
||||
.await
|
||||
{
|
||||
Ok(tx) => tx,
|
||||
Err(err) => {
|
||||
return Err(err);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
self.platform_information.node.execute_transaction(tx).await
|
||||
// Resolve the signer to ensure we use an address that has keys
|
||||
if let Some(from) = tx.from {
|
||||
tx.from = Some(self.platform_information.node.resolve_signer_or_default(from));
|
||||
}
|
||||
|
||||
self.platform_information.node.execute_transaction(tx).await
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,18 +510,13 @@ where
|
||||
};
|
||||
|
||||
// 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),
|
||||
) {
|
||||
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.execution_state
|
||||
.variables
|
||||
.insert(variable_name.clone(), value);
|
||||
self.execution_state.variables.insert(variable_name.clone(), value);
|
||||
tracing::info!(
|
||||
variable_name,
|
||||
variable_value = hex::encode(value.to_be_bytes::<32>()),
|
||||
@@ -534,18 +536,12 @@ where
|
||||
) -> Result<()> {
|
||||
// Resolving the `step.expected` into a series of expectations that we can then assert on.
|
||||
let mut expectations = match step {
|
||||
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: 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()],
|
||||
};
|
||||
|
||||
@@ -664,11 +660,8 @@ where
|
||||
}
|
||||
|
||||
// Handling the topics assertion.
|
||||
for (expected, actual) in expected_event
|
||||
.topics
|
||||
.as_slice()
|
||||
.iter()
|
||||
.zip(actual_event.topics())
|
||||
for (expected, actual) in
|
||||
expected_event.topics.as_slice().iter().zip(actual_event.topics())
|
||||
{
|
||||
let expected = Calldata::new_compound([expected]);
|
||||
if !expected
|
||||
@@ -838,9 +831,7 @@ where
|
||||
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);
|
||||
self.execution_state.variables.insert(variable_name.to_string(), variable);
|
||||
|
||||
Ok(1)
|
||||
}
|
||||
@@ -863,10 +854,8 @@ where
|
||||
calldata: Option<&Calldata>,
|
||||
value: Option<EtherValue>,
|
||||
) -> Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
|
||||
if let Some((_, address, abi)) = self
|
||||
.execution_state
|
||||
.deployed_contracts
|
||||
.get(contract_instance)
|
||||
if let Some((_, address, abi)) =
|
||||
self.execution_state.deployed_contracts.get(contract_instance)
|
||||
{
|
||||
info!(
|
||||
|
||||
@@ -904,19 +893,10 @@ where
|
||||
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)
|
||||
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
|
||||
)
|
||||
anyhow::bail!("Contract source not found for instance {:?}", contract_instance)
|
||||
};
|
||||
|
||||
let Some((code, abi)) = self
|
||||
@@ -926,10 +906,7 @@ where
|
||||
.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
|
||||
)
|
||||
anyhow::bail!("Failed to find information for contract {:?}", contract_instance)
|
||||
};
|
||||
|
||||
let mut code = match alloy::hex::decode(&code) {
|
||||
@@ -942,18 +919,18 @@ where
|
||||
"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 = self.platform_information.node.resolver().await?;
|
||||
let calldata = calldata
|
||||
.calldata(resolver.as_ref(), self.default_resolution_context())
|
||||
.await?;
|
||||
let calldata =
|
||||
calldata.calldata(resolver.as_ref(), self.default_resolution_context()).await?;
|
||||
code.extend(calldata);
|
||||
}
|
||||
|
||||
let tx = {
|
||||
let deployer = self.platform_information.node.resolve_signer_or_default(deployer);
|
||||
let tx = TransactionRequest::default().from(deployer);
|
||||
let tx = match value {
|
||||
Some(ref value) => tx.value(value.into_inner()),
|
||||
@@ -967,7 +944,7 @@ where
|
||||
Err(error) => {
|
||||
tracing::error!(?error, "Contract deployment transaction failed.");
|
||||
return Err(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let Some(address) = receipt.contract_address else {
|
||||
@@ -982,10 +959,9 @@ where
|
||||
.reporter
|
||||
.report_contract_deployed_event(contract_instance.clone(), address)?;
|
||||
|
||||
self.execution_state.deployed_contracts.insert(
|
||||
contract_instance.clone(),
|
||||
(contract_ident, address, abi.clone()),
|
||||
);
|
||||
self.execution_state
|
||||
.deployed_contracts
|
||||
.insert(contract_instance.clone(), (contract_ident, address, abi.clone()));
|
||||
|
||||
Ok((address, abi, receipt))
|
||||
}
|
||||
@@ -998,9 +974,7 @@ where
|
||||
match step_address {
|
||||
StepAddress::Address(address) => Ok(*address),
|
||||
StepAddress::ResolvableAddress(resolvable) => {
|
||||
let Some(instance) = resolvable
|
||||
.strip_suffix(".address")
|
||||
.map(ContractInstance::new)
|
||||
let Some(instance) = resolvable.strip_suffix(".address").map(ContractInstance::new)
|
||||
else {
|
||||
bail!("Not an address variable");
|
||||
};
|
||||
@@ -1013,7 +987,7 @@ where
|
||||
)
|
||||
.await
|
||||
.map(|v| v.0)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
// endregion:Contract Deployment
|
||||
|
||||
@@ -7,15 +7,14 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ansi_term::{ANSIStrings, Color};
|
||||
use crate::Platform;
|
||||
use anyhow::Context as _;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use revive_dt_common::types::PrivateKeyAllocator;
|
||||
use revive_dt_core::Platform;
|
||||
use tokio::sync::{Mutex, RwLock, Semaphore};
|
||||
use tracing::{Instrument, error, info, info_span, instrument};
|
||||
|
||||
use revive_dt_config::{Context, OutputFormat, TestExecutionContext};
|
||||
use revive_dt_config::{Context, TestExecutionContext};
|
||||
use revive_dt_report::{Reporter, ReporterEvent, TestCaseStatus};
|
||||
|
||||
use crate::{
|
||||
@@ -86,13 +85,8 @@ pub async fn handle_differential_tests(
|
||||
|
||||
// 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,
|
||||
context.working_directory.as_path().join("compilation_cache"),
|
||||
context.compilation_configuration.invalidate_compilation_cache,
|
||||
)
|
||||
.await
|
||||
.map(Arc::new)
|
||||
@@ -147,7 +141,7 @@ pub async fn handle_differential_tests(
|
||||
drop(permit);
|
||||
running_task_list.write().await.remove(&test_id);
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
info!("Created the driver for the test case");
|
||||
|
||||
@@ -162,7 +156,7 @@ pub async fn handle_differential_tests(
|
||||
.report_test_failed_event(format!("{error:#}"))
|
||||
.expect("Can't fail");
|
||||
error!("Test Case Failed");
|
||||
}
|
||||
},
|
||||
};
|
||||
info!("Finished the execution of the test case");
|
||||
drop(permit);
|
||||
@@ -173,20 +167,14 @@ pub async fn handle_differential_tests(
|
||||
))
|
||||
.inspect(|_| {
|
||||
info!("Finished executing all test cases");
|
||||
reporter_clone
|
||||
.report_completion_event()
|
||||
.expect("Can't fail")
|
||||
reporter_clone.report_completion_event().expect("Can't fail")
|
||||
});
|
||||
let cli_reporting_task = start_cli_reporting_task(context.output_format, reporter);
|
||||
let cli_reporting_task = start_cli_reporting_task(reporter);
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
loop {
|
||||
let remaining_tasks = running_task_list.read().await;
|
||||
info!(
|
||||
count = remaining_tasks.len(),
|
||||
?remaining_tasks,
|
||||
"Remaining Tests"
|
||||
);
|
||||
info!(count = remaining_tasks.len(), ?remaining_tasks, "Remaining Tests");
|
||||
tokio::time::sleep(Duration::from_secs(10)).await
|
||||
}
|
||||
});
|
||||
@@ -197,15 +185,21 @@ pub async fn handle_differential_tests(
|
||||
}
|
||||
|
||||
#[allow(irrefutable_let_patterns, clippy::uninlined_format_args)]
|
||||
async fn start_cli_reporting_task(output_format: OutputFormat, reporter: Reporter) {
|
||||
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();
|
||||
|
||||
let mut global_success_count = 0;
|
||||
let mut global_failure_count = 0;
|
||||
let mut global_ignore_count = 0;
|
||||
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 {
|
||||
@@ -218,125 +212,55 @@ async fn start_cli_reporting_task(output_format: OutputFormat, reporter: Reporte
|
||||
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;
|
||||
number_of_successes += 1;
|
||||
writeln!(
|
||||
buf,
|
||||
"{}",
|
||||
ANSIStrings(&[
|
||||
Color::Green.bold().paint("Case Succeeded"),
|
||||
Color::Green
|
||||
.paint(format!(" - Steps Executed: {steps_executed}")),
|
||||
])
|
||||
"{}{}Case Succeeded{} - Steps Executed: {}{}",
|
||||
GREEN, BOLD, BOLD_RESET, steps_executed, COLOR_RESET
|
||||
)
|
||||
}
|
||||
},
|
||||
TestCaseStatus::Failed { reason } => {
|
||||
global_failure_count += 1;
|
||||
number_of_failures += 1;
|
||||
writeln!(
|
||||
buf,
|
||||
"{}",
|
||||
ANSIStrings(&[
|
||||
Color::Red.bold().paint("Case Failed"),
|
||||
Color::Red.paint(format!(" - Reason: {}", reason.trim())),
|
||||
])
|
||||
"{}{}Case Failed{} - Reason: {}{}",
|
||||
RED,
|
||||
BOLD,
|
||||
BOLD_RESET,
|
||||
reason.trim(),
|
||||
COLOR_RESET,
|
||||
)
|
||||
}
|
||||
TestCaseStatus::Ignored { reason, .. } => {
|
||||
global_ignore_count += 1;
|
||||
writeln!(
|
||||
},
|
||||
TestCaseStatus::Ignored { reason, .. } => writeln!(
|
||||
buf,
|
||||
"{}",
|
||||
ANSIStrings(&[
|
||||
Color::Yellow.bold().paint("Case Ignored"),
|
||||
Color::Yellow.paint(format!(" - Reason: {}", reason.trim())),
|
||||
])
|
||||
)
|
||||
}
|
||||
"{}{}Case Ignored{} - Reason: {}{}",
|
||||
GREY,
|
||||
BOLD,
|
||||
BOLD_RESET,
|
||||
reason.trim(),
|
||||
COLOR_RESET,
|
||||
),
|
||||
};
|
||||
}
|
||||
let _ = writeln!(buf);
|
||||
}
|
||||
OutputFormat::CargoTestLike => {
|
||||
writeln!(
|
||||
buf,
|
||||
"\t{} {} - {}\n",
|
||||
Color::Green.paint("Running"),
|
||||
metadata_file_path.display(),
|
||||
mode
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut success_count = 0;
|
||||
let mut failure_count = 0;
|
||||
let mut ignored_count = 0;
|
||||
writeln!(buf, "running {} tests", case_status.len()).unwrap();
|
||||
for (case_idx, case_result) in case_status.iter() {
|
||||
let status = match case_result {
|
||||
TestCaseStatus::Succeeded { .. } => {
|
||||
success_count += 1;
|
||||
global_success_count += 1;
|
||||
Color::Green.paint("ok")
|
||||
}
|
||||
TestCaseStatus::Failed { reason } => {
|
||||
failure_count += 1;
|
||||
global_failure_count += 1;
|
||||
Color::Red.paint(format!("FAILED, {reason}"))
|
||||
}
|
||||
TestCaseStatus::Ignored { reason, .. } => {
|
||||
ignored_count += 1;
|
||||
global_ignore_count += 1;
|
||||
Color::Yellow.paint(format!("ignored, {reason:?}"))
|
||||
}
|
||||
};
|
||||
writeln!(buf, "test case_idx_{} ... {}", case_idx, status).unwrap();
|
||||
}
|
||||
writeln!(buf).unwrap();
|
||||
|
||||
let status = if failure_count > 0 {
|
||||
Color::Red.paint("FAILED")
|
||||
} else {
|
||||
Color::Green.paint("ok")
|
||||
};
|
||||
writeln!(
|
||||
buf,
|
||||
"test result: {}. {} passed; {} failed; {} ignored",
|
||||
status, success_count, failure_count, ignored_count,
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(buf).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary at the end.
|
||||
match output_format {
|
||||
OutputFormat::Legacy => {
|
||||
writeln!(
|
||||
let _ = 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()),
|
||||
"{} 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()
|
||||
)
|
||||
.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();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,14 +10,15 @@ 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.
|
||||
/// 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.
|
||||
/// This map stores the variables used for each one of the cases contained in the metadata
|
||||
/// file.
|
||||
pub variables: HashMap<String, U256>,
|
||||
}
|
||||
|
||||
@@ -26,10 +27,6 @@ impl ExecutionState {
|
||||
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
|
||||
deployed_contracts: HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
compiled_contracts,
|
||||
deployed_contracts,
|
||||
variables: Default::default(),
|
||||
}
|
||||
Self { compiled_contracts, deployed_contracts, variables: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ use std::{
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
|
||||
use crate::Platform;
|
||||
use futures::FutureExt;
|
||||
use revive_dt_common::{iterators::FilesWithExtensionIterator, types::CompilerIdentifier};
|
||||
use revive_dt_compiler::{Compiler, CompilerOutput, Mode, SolidityCompiler};
|
||||
use revive_dt_core::Platform;
|
||||
use revive_dt_format::metadata::{ContractIdent, ContractInstance, Metadata};
|
||||
|
||||
use alloy::{hex::ToHexExt, json_abi::JsonAbi, primitives::Address};
|
||||
@@ -41,10 +41,7 @@ impl<'a> CachedCompiler<'a> {
|
||||
.await
|
||||
.context("Failed to invalidate compilation cache directory")?;
|
||||
}
|
||||
Ok(Self {
|
||||
artifacts_cache: cache,
|
||||
cache_key_lock: Default::default(),
|
||||
})
|
||||
Ok(Self { artifacts_cache: cache, cache_key_lock: Default::default() })
|
||||
}
|
||||
|
||||
/// Compiles or gets the compilation artifacts from the cache.
|
||||
@@ -112,7 +109,7 @@ impl<'a> CachedCompiler<'a> {
|
||||
.await
|
||||
.context("Compilation callback for deployed libraries failed")?
|
||||
.compiler_output
|
||||
}
|
||||
},
|
||||
// If no deployed libraries are specified then we can follow the cached flow and attempt
|
||||
// to lookup the compilation artifacts in the cache.
|
||||
None => {
|
||||
@@ -126,7 +123,7 @@ impl<'a> CachedCompiler<'a> {
|
||||
Some(value) => {
|
||||
drop(read_guard);
|
||||
value
|
||||
}
|
||||
},
|
||||
None => {
|
||||
drop(read_guard);
|
||||
self.cache_key_lock
|
||||
@@ -135,7 +132,7 @@ impl<'a> CachedCompiler<'a> {
|
||||
.entry(cache_key.clone())
|
||||
.or_default()
|
||||
.clone()
|
||||
}
|
||||
},
|
||||
};
|
||||
let _guard = mutex.lock().await;
|
||||
|
||||
@@ -163,7 +160,7 @@ impl<'a> CachedCompiler<'a> {
|
||||
.expect("Can't happen");
|
||||
}
|
||||
cache_value.compiler_output
|
||||
}
|
||||
},
|
||||
None => {
|
||||
let compiler_output = compilation_callback()
|
||||
.await
|
||||
@@ -172,18 +169,16 @@ impl<'a> CachedCompiler<'a> {
|
||||
self.artifacts_cache
|
||||
.insert(
|
||||
&cache_key,
|
||||
&CacheValue {
|
||||
compiler_output: compiler_output.clone(),
|
||||
},
|
||||
&CacheValue { compiler_output: compiler_output.clone() },
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to write the cached value of the compilation artifacts",
|
||||
)?;
|
||||
compiler_output
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Ok(compiled_contracts)
|
||||
@@ -225,9 +220,7 @@ async fn compile_contracts(
|
||||
.flat_map(|value| value.iter())
|
||||
.map(|(instance, (ident, address, abi))| (instance, ident, address, abi))
|
||||
.flat_map(|(_, ident, address, _)| {
|
||||
all_sources_in_dir
|
||||
.iter()
|
||||
.map(move |path| (ident, address, path))
|
||||
all_sources_in_dir.iter().map(move |path| (ident, address, path))
|
||||
})
|
||||
.fold(compiler, |compiler, (ident, address, path)| {
|
||||
compiler.with_library(path, ident.as_str(), *address)
|
||||
@@ -248,7 +241,7 @@ async fn compile_contracts(
|
||||
output.clone(),
|
||||
)
|
||||
.expect("Can't happen");
|
||||
}
|
||||
},
|
||||
(Ok(output), false) => {
|
||||
reporter
|
||||
.report_pre_link_contracts_compilation_succeeded_event(
|
||||
@@ -259,7 +252,7 @@ async fn compile_contracts(
|
||||
output.clone(),
|
||||
)
|
||||
.expect("Can't happen");
|
||||
}
|
||||
},
|
||||
(Err(err), true) => {
|
||||
reporter
|
||||
.report_post_link_contracts_compilation_failed_event(
|
||||
@@ -269,7 +262,7 @@ async fn compile_contracts(
|
||||
format!("{err:#}"),
|
||||
)
|
||||
.expect("Can't happen");
|
||||
}
|
||||
},
|
||||
(Err(err), false) => {
|
||||
reporter
|
||||
.report_pre_link_contracts_compilation_failed_event(
|
||||
@@ -279,7 +272,7 @@ async fn compile_contracts(
|
||||
format!("{err:#}"),
|
||||
)
|
||||
.expect("Can't happen");
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
output
|
||||
@@ -291,9 +284,7 @@ struct ArtifactsCache {
|
||||
|
||||
impl ArtifactsCache {
|
||||
pub fn new(path: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
path: path.as_ref().to_path_buf(),
|
||||
}
|
||||
Self { path: path.as_ref().to_path_buf() }
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all, err)]
|
||||
@@ -319,9 +310,7 @@ impl ArtifactsCache {
|
||||
|
||||
pub async fn get(&self, key: &CacheKey<'_>) -> Option<CacheValue> {
|
||||
let key = bson::to_vec(key).ok()?;
|
||||
let value = cacache::read(self.path.as_path(), key.encode_hex())
|
||||
.await
|
||||
.ok()?;
|
||||
let value = cacache::read(self.path.as_path(), key.encode_hex()).await.ok()?;
|
||||
let value = bson::from_slice::<CacheValue>(&value).ok()?;
|
||||
Some(value)
|
||||
}
|
||||
@@ -336,13 +325,13 @@ impl ArtifactsCache {
|
||||
Some(value) => {
|
||||
debug!("Cache hit");
|
||||
Ok(value)
|
||||
}
|
||||
},
|
||||
None => {
|
||||
debug!("Cache miss");
|
||||
let value = callback().await?;
|
||||
self.insert(key, &value).await?;
|
||||
Ok(value)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crate::Platform;
|
||||
use anyhow::Context as _;
|
||||
use revive_dt_config::*;
|
||||
use revive_dt_core::Platform;
|
||||
use revive_dt_node_interaction::EthereumNode;
|
||||
|
||||
/// The node pool starts one or more [Node] which then can be accessed
|
||||
@@ -37,18 +37,13 @@ impl NodePool {
|
||||
);
|
||||
}
|
||||
|
||||
let pre_transactions_tasks = nodes
|
||||
.iter_mut()
|
||||
.map(|node| node.pre_transactions())
|
||||
.collect::<Vec<_>>();
|
||||
let pre_transactions_tasks =
|
||||
nodes.iter_mut().map(|node| node.pre_transactions()).collect::<Vec<_>>();
|
||||
futures::future::try_join_all(pre_transactions_tasks)
|
||||
.await
|
||||
.context("Failed to run the pre-transactions task")?;
|
||||
|
||||
Ok(Self {
|
||||
nodes,
|
||||
next: Default::default(),
|
||||
})
|
||||
Ok(Self { nodes, next: Default::default() })
|
||||
}
|
||||
|
||||
/// Get a handle to the next node.
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::{borrow::Cow, path::Path};
|
||||
use std::{borrow::Cow, collections::BTreeMap, path::Path, sync::Arc};
|
||||
|
||||
use futures::{Stream, StreamExt, stream};
|
||||
use indexmap::{IndexMap, indexmap};
|
||||
use revive_dt_common::iterators::EitherIter;
|
||||
use revive_dt_common::types::PlatformIdentifier;
|
||||
use revive_dt_common::{iterators::EitherIter, types::PlatformIdentifier};
|
||||
use revive_dt_config::Context;
|
||||
use revive_dt_format::mode::ParsedMode;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use revive_dt_compiler::Mode;
|
||||
use revive_dt_compiler::SolidityCompiler;
|
||||
use revive_dt_compiler::{Mode, SolidityCompiler};
|
||||
use revive_dt_format::{
|
||||
case::{Case, CaseIdx},
|
||||
metadata::MetadataFile,
|
||||
};
|
||||
use revive_dt_node_interaction::EthereumNode;
|
||||
use revive_dt_report::{ExecutionSpecificReporter, Reporter};
|
||||
use revive_dt_report::{TestSpecificReporter, TestSpecifier};
|
||||
use revive_dt_report::{ExecutionSpecificReporter, Reporter, TestSpecificReporter, TestSpecifier};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::Platform;
|
||||
use crate::helpers::NodePool;
|
||||
use crate::{Platform, 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
|
||||
@@ -69,18 +63,15 @@ pub async fn create_test_definitions_stream<'a>(
|
||||
)
|
||||
})
|
||||
})
|
||||
// Inform the reporter of each one of the test cases that were discovered which we expect to
|
||||
// run.
|
||||
// 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");
|
||||
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 {
|
||||
.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();
|
||||
@@ -109,12 +100,7 @@ pub async fn create_test_definitions_stream<'a>(
|
||||
|
||||
platforms.insert(
|
||||
platform.platform_identifier(),
|
||||
TestPlatformInformation {
|
||||
platform: *platform,
|
||||
node,
|
||||
compiler,
|
||||
reporter,
|
||||
},
|
||||
TestPlatformInformation { platform: *platform, node, compiler, reporter },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,8 +122,7 @@ pub async fn create_test_definitions_stream<'a>(
|
||||
/* 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() {
|
||||
@@ -162,7 +147,7 @@ pub async fn create_test_definitions_stream<'a>(
|
||||
)
|
||||
.expect("Can't fail");
|
||||
None
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
.inspect(|test| {
|
||||
@@ -236,9 +221,8 @@ impl<'a> TestDefinition<'a> {
|
||||
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())
|
||||
}
|
||||
Some(required_vm_identifiers) =>
|
||||
required_vm_identifiers.contains(&platform_information.platform.vm_identifier()),
|
||||
};
|
||||
is_allowed &= is_allowed_for_platform;
|
||||
error_map.insert(
|
||||
@@ -280,10 +264,7 @@ impl<'a> TestDefinition<'a> {
|
||||
if is_allowed {
|
||||
Ok(())
|
||||
} else {
|
||||
Err((
|
||||
"EVM version is incompatible for the platforms specified",
|
||||
error_map,
|
||||
))
|
||||
Err(("EVM version is incompatible for the platforms specified", error_map))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+51
-85
@@ -3,6 +3,9 @@
|
||||
//! This crate defines the testing configuration and
|
||||
//! provides a helper utility to execute tests.
|
||||
|
||||
pub mod differential_tests;
|
||||
pub mod helpers;
|
||||
|
||||
use std::{
|
||||
pin::Pin,
|
||||
thread::{self, JoinHandle},
|
||||
@@ -14,13 +17,17 @@ use revive_dt_common::types::*;
|
||||
use revive_dt_compiler::{SolidityCompiler, revive_resolc::Resolc, solc::Solc};
|
||||
use revive_dt_config::*;
|
||||
use revive_dt_node::{
|
||||
Node, node_implementations::geth::GethNode,
|
||||
node_implementations::lighthouse_geth::LighthouseGethNode,
|
||||
node_implementations::substrate::SubstrateNode, node_implementations::zombienet::ZombienetNode,
|
||||
Node,
|
||||
node_implementations::{
|
||||
geth::GethNode, lighthouse_geth::LighthouseGethNode, substrate::SubstrateNode,
|
||||
zombienet::ZombieNode,
|
||||
},
|
||||
};
|
||||
use revive_dt_node_interaction::EthereumNode;
|
||||
use tracing::info;
|
||||
|
||||
pub use helpers::CachedCompiler;
|
||||
|
||||
/// A trait that describes the interface for the platforms that are supported by the tool.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub trait Platform {
|
||||
@@ -30,11 +37,7 @@ pub trait Platform {
|
||||
|
||||
/// Returns a full identifier for the platform.
|
||||
fn full_identifier(&self) -> (NodeIdentifier, VmIdentifier, CompilerIdentifier) {
|
||||
(
|
||||
self.node_identifier(),
|
||||
self.vm_identifier(),
|
||||
self.compiler_identifier(),
|
||||
)
|
||||
(self.node_identifier(), self.vm_identifier(), self.compiler_identifier())
|
||||
}
|
||||
|
||||
/// Returns the identifier of the node used.
|
||||
@@ -176,15 +179,12 @@ impl Platform for KitchensinkPolkavmResolcPlatform {
|
||||
context: Context,
|
||||
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
|
||||
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
||||
let kitchensink_path = AsRef::<KitchensinkConfiguration>::as_ref(&context)
|
||||
.path
|
||||
.clone();
|
||||
let kitchensink_path = AsRef::<KitchensinkConfiguration>::as_ref(&context).path.clone();
|
||||
let genesis = genesis_configuration.genesis()?.clone();
|
||||
Ok(thread::spawn(move || {
|
||||
let node = SubstrateNode::new(
|
||||
kitchensink_path,
|
||||
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
|
||||
None,
|
||||
context,
|
||||
);
|
||||
let node = spawn_node(node, genesis)?;
|
||||
@@ -229,15 +229,12 @@ impl Platform for KitchensinkRevmSolcPlatform {
|
||||
context: Context,
|
||||
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
|
||||
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
||||
let kitchensink_path = AsRef::<KitchensinkConfiguration>::as_ref(&context)
|
||||
.path
|
||||
.clone();
|
||||
let kitchensink_path = AsRef::<KitchensinkConfiguration>::as_ref(&context).path.clone();
|
||||
let genesis = genesis_configuration.genesis()?.clone();
|
||||
Ok(thread::spawn(move || {
|
||||
let node = SubstrateNode::new(
|
||||
kitchensink_path,
|
||||
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
|
||||
None,
|
||||
context,
|
||||
);
|
||||
let node = spawn_node(node, genesis)?;
|
||||
@@ -282,17 +279,13 @@ impl Platform for ReviveDevNodePolkavmResolcPlatform {
|
||||
context: Context,
|
||||
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
|
||||
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
||||
let revive_dev_node_configuration = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context);
|
||||
|
||||
let revive_dev_node_path = revive_dev_node_configuration.path.clone();
|
||||
let revive_dev_node_consensus = revive_dev_node_configuration.consensus.clone();
|
||||
|
||||
let revive_dev_node_path =
|
||||
AsRef::<ReviveDevNodeConfiguration>::as_ref(&context).path.clone();
|
||||
let genesis = genesis_configuration.genesis()?.clone();
|
||||
Ok(thread::spawn(move || {
|
||||
let node = SubstrateNode::new(
|
||||
revive_dev_node_path,
|
||||
SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND,
|
||||
Some(revive_dev_node_consensus),
|
||||
context,
|
||||
);
|
||||
let node = spawn_node(node, genesis)?;
|
||||
@@ -337,17 +330,13 @@ impl Platform for ReviveDevNodeRevmSolcPlatform {
|
||||
context: Context,
|
||||
) -> anyhow::Result<JoinHandle<anyhow::Result<Box<dyn EthereumNode + Send + Sync>>>> {
|
||||
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
|
||||
let revive_dev_node_configuration = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context);
|
||||
|
||||
let revive_dev_node_path = revive_dev_node_configuration.path.clone();
|
||||
let revive_dev_node_consensus = revive_dev_node_configuration.consensus.clone();
|
||||
|
||||
let revive_dev_node_path =
|
||||
AsRef::<ReviveDevNodeConfiguration>::as_ref(&context).path.clone();
|
||||
let genesis = genesis_configuration.genesis()?.clone();
|
||||
Ok(thread::spawn(move || {
|
||||
let node = SubstrateNode::new(
|
||||
revive_dev_node_path,
|
||||
SubstrateNode::REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND,
|
||||
Some(revive_dev_node_consensus),
|
||||
context,
|
||||
);
|
||||
let node = spawn_node(node, genesis)?;
|
||||
@@ -392,12 +381,11 @@ impl Platform for ZombienetPolkavmResolcPlatform {
|
||||
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 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 = ZombieNode::new(polkadot_parachain_path, context);
|
||||
let node = spawn_node(node, genesis)?;
|
||||
Ok(Box::new(node) as Box<_>)
|
||||
}))
|
||||
@@ -409,7 +397,7 @@ impl Platform for ZombienetPolkavmResolcPlatform {
|
||||
version: Option<VersionOrRequirement>,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn SolidityCompiler>>>>> {
|
||||
Box::pin(async move {
|
||||
let compiler = Resolc::new(context, version).await;
|
||||
let compiler = Solc::new(context, version).await;
|
||||
compiler.map(|compiler| Box::new(compiler) as Box<dyn SolidityCompiler>)
|
||||
})
|
||||
}
|
||||
@@ -440,12 +428,11 @@ impl Platform for ZombienetRevmSolcPlatform {
|
||||
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 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 = ZombieNode::new(polkadot_parachain_path, context);
|
||||
let node = spawn_node(node, genesis)?;
|
||||
Ok(Box::new(node) as Box<_>)
|
||||
}))
|
||||
@@ -467,24 +454,18 @@ impl From<PlatformIdentifier> for Box<dyn Platform> {
|
||||
fn from(value: PlatformIdentifier) -> Self {
|
||||
match value {
|
||||
PlatformIdentifier::GethEvmSolc => Box::new(GethEvmSolcPlatform) as Box<_>,
|
||||
PlatformIdentifier::LighthouseGethEvmSolc => {
|
||||
Box::new(LighthouseGethEvmSolcPlatform) as Box<_>
|
||||
}
|
||||
PlatformIdentifier::KitchensinkPolkavmResolc => {
|
||||
Box::new(KitchensinkPolkavmResolcPlatform) as Box<_>
|
||||
}
|
||||
PlatformIdentifier::KitchensinkRevmSolc => {
|
||||
Box::new(KitchensinkRevmSolcPlatform) as Box<_>
|
||||
}
|
||||
PlatformIdentifier::ReviveDevNodePolkavmResolc => {
|
||||
Box::new(ReviveDevNodePolkavmResolcPlatform) as Box<_>
|
||||
}
|
||||
PlatformIdentifier::ReviveDevNodeRevmSolc => {
|
||||
Box::new(ReviveDevNodeRevmSolcPlatform) as Box<_>
|
||||
}
|
||||
PlatformIdentifier::ZombienetPolkavmResolc => {
|
||||
Box::new(ZombienetPolkavmResolcPlatform) as Box<_>
|
||||
}
|
||||
PlatformIdentifier::LighthouseGethEvmSolc =>
|
||||
Box::new(LighthouseGethEvmSolcPlatform) as Box<_>,
|
||||
PlatformIdentifier::KitchensinkPolkavmResolc =>
|
||||
Box::new(KitchensinkPolkavmResolcPlatform) as Box<_>,
|
||||
PlatformIdentifier::KitchensinkRevmSolc =>
|
||||
Box::new(KitchensinkRevmSolcPlatform) as Box<_>,
|
||||
PlatformIdentifier::ReviveDevNodePolkavmResolc =>
|
||||
Box::new(ReviveDevNodePolkavmResolcPlatform) as Box<_>,
|
||||
PlatformIdentifier::ReviveDevNodeRevmSolc =>
|
||||
Box::new(ReviveDevNodeRevmSolcPlatform) as Box<_>,
|
||||
PlatformIdentifier::ZombienetPolkavmResolc =>
|
||||
Box::new(ZombienetPolkavmResolcPlatform) as Box<_>,
|
||||
PlatformIdentifier::ZombienetRevmSolc => Box::new(ZombienetRevmSolcPlatform) as Box<_>,
|
||||
}
|
||||
}
|
||||
@@ -494,24 +475,18 @@ impl From<PlatformIdentifier> for &dyn Platform {
|
||||
fn from(value: PlatformIdentifier) -> Self {
|
||||
match value {
|
||||
PlatformIdentifier::GethEvmSolc => &GethEvmSolcPlatform as &dyn Platform,
|
||||
PlatformIdentifier::LighthouseGethEvmSolc => {
|
||||
&LighthouseGethEvmSolcPlatform as &dyn Platform
|
||||
}
|
||||
PlatformIdentifier::KitchensinkPolkavmResolc => {
|
||||
&KitchensinkPolkavmResolcPlatform as &dyn Platform
|
||||
}
|
||||
PlatformIdentifier::KitchensinkRevmSolc => {
|
||||
&KitchensinkRevmSolcPlatform as &dyn Platform
|
||||
}
|
||||
PlatformIdentifier::ReviveDevNodePolkavmResolc => {
|
||||
&ReviveDevNodePolkavmResolcPlatform as &dyn Platform
|
||||
}
|
||||
PlatformIdentifier::ReviveDevNodeRevmSolc => {
|
||||
&ReviveDevNodeRevmSolcPlatform as &dyn Platform
|
||||
}
|
||||
PlatformIdentifier::ZombienetPolkavmResolc => {
|
||||
&ZombienetPolkavmResolcPlatform as &dyn Platform
|
||||
}
|
||||
PlatformIdentifier::LighthouseGethEvmSolc =>
|
||||
&LighthouseGethEvmSolcPlatform as &dyn Platform,
|
||||
PlatformIdentifier::KitchensinkPolkavmResolc =>
|
||||
&KitchensinkPolkavmResolcPlatform as &dyn Platform,
|
||||
PlatformIdentifier::KitchensinkRevmSolc =>
|
||||
&KitchensinkRevmSolcPlatform as &dyn Platform,
|
||||
PlatformIdentifier::ReviveDevNodePolkavmResolc =>
|
||||
&ReviveDevNodePolkavmResolcPlatform as &dyn Platform,
|
||||
PlatformIdentifier::ReviveDevNodeRevmSolc =>
|
||||
&ReviveDevNodeRevmSolcPlatform as &dyn Platform,
|
||||
PlatformIdentifier::ZombienetPolkavmResolc =>
|
||||
&ZombienetPolkavmResolcPlatform as &dyn Platform,
|
||||
PlatformIdentifier::ZombienetRevmSolc => &ZombienetRevmSolcPlatform as &dyn Platform,
|
||||
}
|
||||
}
|
||||
@@ -521,17 +496,8 @@ fn spawn_node<T: Node + EthereumNode + Send + Sync>(
|
||||
mut node: T,
|
||||
genesis: Genesis,
|
||||
) -> anyhow::Result<T> {
|
||||
info!(
|
||||
id = node.id(),
|
||||
connection_string = node.connection_string(),
|
||||
"Spawning node"
|
||||
);
|
||||
node.spawn(genesis)
|
||||
.context("Failed to spawn node process")?;
|
||||
info!(
|
||||
id = node.id(),
|
||||
connection_string = node.connection_string(),
|
||||
"Spawned node"
|
||||
);
|
||||
info!(id = node.id(), connection_string = node.connection_string(), "Spawning node");
|
||||
node.spawn(genesis).context("Failed to spawn node process")?;
|
||||
info!(id = node.id(), connection_string = node.connection_string(), "Spawned node");
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
@@ -76,6 +76,6 @@ fn main() -> anyhow::Result<()> {
|
||||
let schema = schema_for!(Metadata);
|
||||
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ pub struct Case {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expected: Option<Expected>,
|
||||
|
||||
/// An optional boolean which defines if the case as a whole should be ignored. If null then the
|
||||
/// case will not be ignored.
|
||||
/// An optional boolean which defines if the case as a whole should be ignored. If null then
|
||||
/// the case will not be ignored.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ignore: Option<bool>,
|
||||
}
|
||||
@@ -54,11 +54,7 @@ pub struct Case {
|
||||
impl Case {
|
||||
pub fn steps_iterator(&self) -> impl Iterator<Item = Step> {
|
||||
let steps_len = self.steps.len();
|
||||
self.steps
|
||||
.clone()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(move |(idx, mut step)| {
|
||||
self.steps.clone().into_iter().enumerate().map(move |(idx, mut step)| {
|
||||
let Step::FunctionCall(ref mut input) = step else {
|
||||
return step;
|
||||
};
|
||||
@@ -84,9 +80,7 @@ impl Case {
|
||||
&self,
|
||||
default_repeat_count: usize,
|
||||
) -> Box<dyn Iterator<Item = Step> + '_> {
|
||||
let contains_repeat = self
|
||||
.steps_iterator()
|
||||
.any(|step| matches!(&step, Step::Repeat(..)));
|
||||
let contains_repeat = self.steps_iterator().any(|step| matches!(&step, Step::Repeat(..)));
|
||||
if contains_repeat {
|
||||
Box::new(self.steps_iterator()) as Box<_>
|
||||
} else {
|
||||
|
||||
@@ -86,11 +86,7 @@ impl Corpus {
|
||||
.collect::<Vec<_>>();
|
||||
tests.sort_by(|a, b| a.metadata_file_path.cmp(&b.metadata_file_path));
|
||||
tests.dedup_by(|a, b| a.metadata_file_path == b.metadata_file_path);
|
||||
info!(
|
||||
len = tests.len(),
|
||||
corpus_name = self.name(),
|
||||
"Found tests in Corpus"
|
||||
);
|
||||
info!(len = tests.len(), corpus_name = self.name(), "Found tests in Corpus");
|
||||
tests
|
||||
}
|
||||
|
||||
@@ -102,23 +98,19 @@ impl Corpus {
|
||||
|
||||
pub fn paths_iter(&self) -> impl Iterator<Item = &Path> {
|
||||
match self {
|
||||
Corpus::SinglePath { path, .. } => {
|
||||
Box::new(std::iter::once(path.as_path())) as Box<dyn Iterator<Item = _>>
|
||||
}
|
||||
Corpus::MultiplePaths { paths, .. } => {
|
||||
Box::new(paths.iter().map(|path| path.as_path())) as Box<dyn Iterator<Item = _>>
|
||||
}
|
||||
Corpus::SinglePath { path, .. } =>
|
||||
Box::new(std::iter::once(path.as_path())) as Box<dyn Iterator<Item = _>>,
|
||||
Corpus::MultiplePaths { paths, .. } =>
|
||||
Box::new(paths.iter().map(|path| path.as_path())) as Box<dyn Iterator<Item = _>>,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paths_iter_mut(&mut self) -> impl Iterator<Item = &mut PathBuf> {
|
||||
match self {
|
||||
Corpus::SinglePath { path, .. } => {
|
||||
Box::new(std::iter::once(path)) as Box<dyn Iterator<Item = _>>
|
||||
}
|
||||
Corpus::MultiplePaths { paths, .. } => {
|
||||
Box::new(paths.iter_mut()) as Box<dyn Iterator<Item = _>>
|
||||
}
|
||||
Corpus::SinglePath { path, .. } =>
|
||||
Box::new(std::iter::once(path)) as Box<dyn Iterator<Item = _>>,
|
||||
Corpus::MultiplePaths { paths, .. } =>
|
||||
Box::new(paths.iter_mut()) as Box<dyn Iterator<Item = _>>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ pub struct MetadataFile {
|
||||
/// The path of the metadata file. This will either be a JSON or solidity file.
|
||||
pub metadata_file_path: PathBuf,
|
||||
|
||||
/// This is the path contained within the corpus file. This could either be the path of some dir
|
||||
/// or could be the actual metadata file path.
|
||||
/// This is the path contained within the corpus file. This could either be the path of some
|
||||
/// dir or could be the actual metadata file path.
|
||||
pub corpus_file_path: PathBuf,
|
||||
|
||||
/// The metadata contained within the file.
|
||||
@@ -44,9 +44,7 @@ impl MetadataFile {
|
||||
if self.corpus_file_path.is_file() {
|
||||
&self.corpus_file_path
|
||||
} else {
|
||||
self.metadata_file_path
|
||||
.strip_prefix(&self.corpus_file_path)
|
||||
.unwrap()
|
||||
self.metadata_file_path.strip_prefix(&self.corpus_file_path).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,13 +67,13 @@ impl Deref for MetadataFile {
|
||||
/// of steps and assertions that should be performed as part of the test case.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema, Clone, Eq, PartialEq)]
|
||||
pub struct Metadata {
|
||||
/// This is an optional comment on the metadata file which has no impact on the execution in any
|
||||
/// way.
|
||||
/// This is an optional comment on the metadata file which has no impact on the execution in
|
||||
/// any way.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
|
||||
/// An optional boolean which defines if the metadata file as a whole should be ignored. If null
|
||||
/// then the metadata file will not be ignored.
|
||||
/// An optional boolean which defines if the metadata file as a whole should be ignored. If
|
||||
/// null then the metadata file will not be ignored.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ignore: Option<bool>,
|
||||
|
||||
@@ -94,8 +92,8 @@ pub struct Metadata {
|
||||
/// This is a map where the key is the name of the contract instance and the value is the
|
||||
/// contract's path and ident in the file.
|
||||
///
|
||||
/// If any contract is to be used by the test then it must be included in here first so that the
|
||||
/// framework is aware of its path, compiles it, and prepares it.
|
||||
/// If any contract is to be used by the test then it must be included in here first so that
|
||||
/// the framework is aware of its path, compiles it, and prepares it.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>,
|
||||
|
||||
@@ -123,8 +121,9 @@ pub struct Metadata {
|
||||
pub required_evm_version: Option<EvmVersionRequirement>,
|
||||
|
||||
/// A set of compilation directives that will be passed to the compiler whenever the contracts
|
||||
/// for the test are being compiled. Note that this differs from the [`Mode`]s in that a [`Mode`]
|
||||
/// is just a filter for when a test can run whereas this is an instruction to the compiler.
|
||||
/// for the test are being compiled. Note that this differs from the [`Mode`]s in that a
|
||||
/// [`Mode`] is just a filter for when a test can run whereas this is an instruction to the
|
||||
/// compiler.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub compiler_directives: Option<CompilationDirectives>,
|
||||
}
|
||||
@@ -158,19 +157,10 @@ impl Metadata {
|
||||
return Ok(sources);
|
||||
};
|
||||
|
||||
for (
|
||||
alias,
|
||||
ContractPathAndIdent {
|
||||
contract_source_path,
|
||||
contract_ident,
|
||||
},
|
||||
) in contracts
|
||||
{
|
||||
for (alias, ContractPathAndIdent { contract_source_path, contract_ident }) in contracts {
|
||||
let alias = alias.clone();
|
||||
let absolute_path = directory
|
||||
.join(contract_source_path)
|
||||
.canonicalize()
|
||||
.map_err(|error| {
|
||||
let absolute_path =
|
||||
directory.join(contract_source_path).canonicalize().map_err(|error| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to canonicalize contract source path '{}': {error}",
|
||||
directory.join(contract_source_path).display()
|
||||
@@ -180,10 +170,7 @@ impl Metadata {
|
||||
|
||||
sources.insert(
|
||||
alias,
|
||||
ContractPathAndIdent {
|
||||
contract_source_path: absolute_path,
|
||||
contract_ident,
|
||||
},
|
||||
ContractPathAndIdent { contract_source_path: absolute_path, contract_ident },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,11 +208,11 @@ impl Metadata {
|
||||
Ok(mut metadata) => {
|
||||
metadata.file_path = Some(path.to_path_buf());
|
||||
Some(metadata)
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!(path = %path.display(), %err, "Deserialization of metadata failed");
|
||||
None
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,11 +245,11 @@ impl Metadata {
|
||||
.into(),
|
||||
);
|
||||
Some(metadata)
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!(path = %path.display(), %err, "Failed to deserialize metadata");
|
||||
None
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +313,8 @@ define_wrapper_type!(
|
||||
)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct ContractPathAndIdent {
|
||||
/// The path of the contract source code relative to the directory containing the metadata file.
|
||||
/// The path of the contract source code relative to the directory containing the metadata
|
||||
/// file.
|
||||
pub contract_source_path: PathBuf,
|
||||
|
||||
/// The identifier of the contract.
|
||||
@@ -335,12 +323,7 @@ pub struct ContractPathAndIdent {
|
||||
|
||||
impl Display for ContractPathAndIdent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}:{}",
|
||||
self.contract_source_path.display(),
|
||||
self.contract_ident.as_ref()
|
||||
)
|
||||
write!(f, "{}:{}", self.contract_source_path.display(), self.contract_ident.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +343,7 @@ impl FromStr for ContractPathAndIdent {
|
||||
Some(ref mut path) => {
|
||||
path.push(':');
|
||||
path.push_str(next_item);
|
||||
}
|
||||
},
|
||||
None => path = Some(next_item.to_owned()),
|
||||
}
|
||||
} else {
|
||||
@@ -380,7 +363,7 @@ impl FromStr for ContractPathAndIdent {
|
||||
contract_source_path: PathBuf::from(path),
|
||||
contract_ident: ContractIdent::new(identifier),
|
||||
})
|
||||
}
|
||||
},
|
||||
(None, None) => anyhow::bail!("Failed to find the path and identifier"),
|
||||
}
|
||||
}
|
||||
@@ -418,43 +401,23 @@ pub struct EvmVersionRequirement {
|
||||
|
||||
impl EvmVersionRequirement {
|
||||
pub fn new_greater_than_or_equals(version: EVMVersion) -> Self {
|
||||
Self {
|
||||
ordering: Ordering::Greater,
|
||||
or_equal: true,
|
||||
evm_version: version,
|
||||
}
|
||||
Self { ordering: Ordering::Greater, or_equal: true, evm_version: version }
|
||||
}
|
||||
|
||||
pub fn new_greater_than(version: EVMVersion) -> Self {
|
||||
Self {
|
||||
ordering: Ordering::Greater,
|
||||
or_equal: false,
|
||||
evm_version: version,
|
||||
}
|
||||
Self { ordering: Ordering::Greater, or_equal: false, evm_version: version }
|
||||
}
|
||||
|
||||
pub fn new_equals(version: EVMVersion) -> Self {
|
||||
Self {
|
||||
ordering: Ordering::Equal,
|
||||
or_equal: false,
|
||||
evm_version: version,
|
||||
}
|
||||
Self { ordering: Ordering::Equal, or_equal: false, evm_version: version }
|
||||
}
|
||||
|
||||
pub fn new_less_than(version: EVMVersion) -> Self {
|
||||
Self {
|
||||
ordering: Ordering::Less,
|
||||
or_equal: false,
|
||||
evm_version: version,
|
||||
}
|
||||
Self { ordering: Ordering::Less, or_equal: false, evm_version: version }
|
||||
}
|
||||
|
||||
pub fn new_less_than_or_equals(version: EVMVersion) -> Self {
|
||||
Self {
|
||||
ordering: Ordering::Less,
|
||||
or_equal: true,
|
||||
evm_version: version,
|
||||
}
|
||||
Self { ordering: Ordering::Less, or_equal: true, evm_version: version }
|
||||
}
|
||||
|
||||
pub fn matches(&self, other: &EVMVersion) -> bool {
|
||||
@@ -465,11 +428,7 @@ impl EvmVersionRequirement {
|
||||
|
||||
impl Display for EvmVersionRequirement {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let Self {
|
||||
ordering,
|
||||
or_equal,
|
||||
evm_version,
|
||||
} = self;
|
||||
let Self { ordering, or_equal, evm_version } = self;
|
||||
match ordering {
|
||||
Ordering::Less => write!(f, "<")?,
|
||||
Ordering::Equal => write!(f, "=")?,
|
||||
@@ -596,10 +555,7 @@ mod test {
|
||||
|
||||
// Assert
|
||||
let identifier = identifier.expect("Failed to parse");
|
||||
assert_eq!(
|
||||
identifier.contract_source_path.display().to_string(),
|
||||
"ERC20/ERC20.sol"
|
||||
);
|
||||
assert_eq!(identifier.contract_source_path.display().to_string(), "ERC20/ERC20.sol");
|
||||
assert_eq!(identifier.contract_ident, "ERC20".to_owned().into());
|
||||
|
||||
// Act
|
||||
|
||||
+11
-26
@@ -1,13 +1,12 @@
|
||||
use anyhow::Context as _;
|
||||
use regex::Regex;
|
||||
use revive_dt_common::iterators::EitherIter;
|
||||
use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
|
||||
use revive_dt_common::{
|
||||
iterators::EitherIter,
|
||||
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;
|
||||
use std::{collections::HashSet, fmt::Display, str::FromStr, sync::LazyLock};
|
||||
|
||||
/// This represents a mode that has been parsed from test metadata.
|
||||
///
|
||||
@@ -78,12 +77,7 @@ impl FromStr for ParsedMode {
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(ParsedMode {
|
||||
pipeline,
|
||||
optimize_flag,
|
||||
optimize_setting,
|
||||
version,
|
||||
})
|
||||
Ok(ParsedMode { pipeline, optimize_flag, optimize_setting, version })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,13 +133,9 @@ impl ParsedMode {
|
||||
|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_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)),
|
||||
@@ -158,9 +148,7 @@ impl ParsedMode {
|
||||
);
|
||||
|
||||
pipeline_iter.flat_map(move |pipeline| {
|
||||
optimize_settings_iter
|
||||
.clone()
|
||||
.map(move |optimize_setting| Mode {
|
||||
optimize_settings_iter.clone().map(move |optimize_setting| Mode {
|
||||
pipeline,
|
||||
optimize_setting,
|
||||
version: self.version.clone(),
|
||||
@@ -236,10 +224,7 @@ mod tests {
|
||||
("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"],
|
||||
),
|
||||
("<=0.8", vec!["Y M0 <=0.8", "Y M3 <=0.8", "E M0 <=0.8", "E M3 <=0.8"]),
|
||||
];
|
||||
|
||||
for (actual, expected) in strings {
|
||||
|
||||
+57
-126
@@ -1,11 +1,10 @@
|
||||
use std::{collections::HashMap, fmt::Display, str::FromStr};
|
||||
|
||||
use alloy::primitives::{FixedBytes, utils::parse_units};
|
||||
use alloy::{
|
||||
eips::BlockNumberOrTag,
|
||||
json_abi::Function,
|
||||
network::TransactionBuilder,
|
||||
primitives::{Address, Bytes, U256},
|
||||
primitives::{Address, Bytes, FixedBytes, U256, utils::parse_units},
|
||||
rpc::types::TransactionRequest,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
@@ -17,8 +16,10 @@ use serde::{Deserialize, Serialize};
|
||||
use revive_dt_common::macros::define_wrapper_type;
|
||||
use tracing::{Instrument, info_span, instrument};
|
||||
|
||||
use crate::traits::ResolverApi;
|
||||
use crate::{metadata::ContractInstance, traits::ResolutionContext};
|
||||
use crate::{
|
||||
metadata::ContractInstance,
|
||||
traits::{ResolutionContext, ResolverApi},
|
||||
};
|
||||
|
||||
/// A test step.
|
||||
///
|
||||
@@ -77,12 +78,7 @@ impl StepPath {
|
||||
|
||||
impl Display for StepPath {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|idx| idx.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(".")
|
||||
.fmt(f)
|
||||
self.0.iter().map(|idx| idx.to_string()).collect::<Vec<_>>().join(".").fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,8 +143,8 @@ pub struct FunctionCallStep {
|
||||
#[schemars(skip)]
|
||||
pub storage: Option<HashMap<String, Calldata>>,
|
||||
|
||||
/// Variable assignment to perform in the framework allowing us to reference them again later on
|
||||
/// during the execution.
|
||||
/// Variable assignment to perform in the framework allowing us to reference them again later
|
||||
/// on during the execution.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub variable_assignments: Option<VariableAssignments>,
|
||||
}
|
||||
@@ -455,9 +451,7 @@ impl StepAddress {
|
||||
|
||||
impl FunctionCallStep {
|
||||
pub const fn default_caller_address() -> Address {
|
||||
Address(FixedBytes(alloy::hex!(
|
||||
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
|
||||
)))
|
||||
Address(FixedBytes(alloy::hex!("0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1")))
|
||||
}
|
||||
|
||||
pub const fn default_caller() -> StepAddress {
|
||||
@@ -482,20 +476,19 @@ impl FunctionCallStep {
|
||||
.context("Failed to produce calldata for deployer/fallback method")?;
|
||||
|
||||
Ok(calldata.into())
|
||||
}
|
||||
},
|
||||
Method::FunctionName(ref function_name) => {
|
||||
let Some(abi) = context.deployed_contract_abi(&self.instance) else {
|
||||
anyhow::bail!("ABI for instance '{}' not found", self.instance.as_ref());
|
||||
};
|
||||
|
||||
// We follow the same logic that's implemented in the matter-labs-tester where they resolve
|
||||
// the function name into a function selector and they assume that he function doesn't have
|
||||
// any existing overloads.
|
||||
// We follow the same logic that's implemented in the matter-labs-tester where they
|
||||
// resolve the function name into a function selector and they assume that he
|
||||
// function doesn't have any existing overloads.
|
||||
// Overloads are handled by providing the full function signature in the "function
|
||||
// name".
|
||||
// https://github.com/matter-labs/era-compiler-tester/blob/1dfa7d07cba0734ca97e24704f12dd57f6990c2c/compiler_tester/src/test/case/input/mod.rs#L158-L190
|
||||
let selector =
|
||||
if function_name.contains('(') && function_name.contains(')') {
|
||||
let selector = if function_name.contains('(') && function_name.contains(')') {
|
||||
Function::parse(function_name)
|
||||
.context(
|
||||
"Failed to parse the provided function name into a function signature",
|
||||
@@ -511,19 +504,21 @@ impl FunctionCallStep {
|
||||
&self.instance
|
||||
)
|
||||
})
|
||||
.with_context(|| format!(
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to resolve function selector for {:?} on instance {:?}",
|
||||
function_name, &self.instance
|
||||
))?
|
||||
)
|
||||
})?
|
||||
.selector()
|
||||
};
|
||||
|
||||
// Allocating a vector that we will be using for the calldata. The vector size will be:
|
||||
// 4 bytes for the function selector.
|
||||
// Allocating a vector that we will be using for the calldata. The vector size will
|
||||
// be: 4 bytes for the function selector.
|
||||
// function.inputs.len() * 32 bytes for the arguments (each argument is a U256).
|
||||
//
|
||||
// We're using indices in the following code in order to avoid the need for us to allocate
|
||||
// a new buffer for each one of the resolved arguments.
|
||||
// We're using indices in the following code in order to avoid the need for us to
|
||||
// allocate a new buffer for each one of the resolved arguments.
|
||||
let mut calldata = Vec::<u8>::with_capacity(4 + self.calldata.size_requirement());
|
||||
calldata.extend(selector.0);
|
||||
self.calldata
|
||||
@@ -532,7 +527,7 @@ impl FunctionCallStep {
|
||||
.context("Failed to append encoded argument to calldata buffer")?;
|
||||
|
||||
Ok(calldata.into())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,11 +542,9 @@ impl FunctionCallStep {
|
||||
.await
|
||||
.context("Failed to encode input bytes for transaction request")?;
|
||||
let caller = self.caller.resolve_address(resolver, context).await?;
|
||||
let transaction_request = TransactionRequest::default().from(caller).value(
|
||||
self.value
|
||||
.map(|value| value.into_inner())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
let transaction_request = TransactionRequest::default()
|
||||
.from(caller)
|
||||
.value(self.value.map(|value| value.into_inner()).unwrap_or_default());
|
||||
match self.method {
|
||||
Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)),
|
||||
_ => Ok(transaction_request
|
||||
@@ -633,8 +626,7 @@ impl Calldata {
|
||||
context: ResolutionContext<'_>,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
let mut buffer = Vec::<u8>::with_capacity(self.size_requirement());
|
||||
self.calldata_into_slice(&mut buffer, resolver, context)
|
||||
.await?;
|
||||
self.calldata_into_slice(&mut buffer, resolver, context).await?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
@@ -647,7 +639,7 @@ impl Calldata {
|
||||
match self {
|
||||
Calldata::Single(bytes) => {
|
||||
buffer.extend_from_slice(bytes);
|
||||
}
|
||||
},
|
||||
Calldata::Compound(items) => {
|
||||
let resolved = stream::iter(items.iter().enumerate())
|
||||
.map(|(arg_idx, arg)| async move {
|
||||
@@ -662,7 +654,7 @@ impl Calldata {
|
||||
.context("Failed to resolve one or more calldata arguments")?;
|
||||
|
||||
buffer.extend(resolved.into_iter().flatten());
|
||||
}
|
||||
},
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
@@ -711,7 +703,7 @@ impl Calldata {
|
||||
.all(|v| async move { v.is_ok_and(|v| v) })
|
||||
.map(Ok)
|
||||
.await
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -725,10 +717,7 @@ impl CalldataItem {
|
||||
) -> anyhow::Result<U256> {
|
||||
let mut stack = Vec::<CalldataToken<U256>>::new();
|
||||
|
||||
for token in self
|
||||
.calldata_tokens()
|
||||
.map(|token| token.resolve(resolver, context))
|
||||
{
|
||||
for token in self.calldata_tokens().map(|token| token.resolve(resolver, context)) {
|
||||
let token = token.await?;
|
||||
let new_token = match token {
|
||||
CalldataToken::Item(_) => token,
|
||||
@@ -750,17 +739,15 @@ impl CalldataItem {
|
||||
Operation::BitwiseAnd => Some(left_operand & right_operand),
|
||||
Operation::BitwiseOr => Some(left_operand | right_operand),
|
||||
Operation::BitwiseXor => Some(left_operand ^ right_operand),
|
||||
Operation::ShiftLeft => {
|
||||
Some(left_operand << usize::try_from(right_operand)?)
|
||||
}
|
||||
Operation::ShiftRight => {
|
||||
Some(left_operand >> usize::try_from(right_operand)?)
|
||||
}
|
||||
Operation::ShiftLeft =>
|
||||
Some(left_operand << usize::try_from(right_operand)?),
|
||||
Operation::ShiftRight =>
|
||||
Some(left_operand >> usize::try_from(right_operand)?),
|
||||
}
|
||||
.context("Invalid calldata arithmetic operation - Invalid operation")?;
|
||||
|
||||
CalldataToken::Item(result)
|
||||
}
|
||||
},
|
||||
};
|
||||
stack.push(new_token)
|
||||
}
|
||||
@@ -769,9 +756,7 @@ impl CalldataItem {
|
||||
// Empty stack means that we got an empty compound calldata which we resolve to zero.
|
||||
[] => Ok(U256::ZERO),
|
||||
[CalldataToken::Item(item)] => Ok(*item),
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"Invalid calldata arithmetic operation - Invalid stack"
|
||||
)),
|
||||
_ => Err(anyhow::anyhow!("Invalid calldata arithmetic operation - Invalid stack")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -915,16 +900,13 @@ impl<T: AsRef<str>> CalldataToken<T> {
|
||||
.await
|
||||
.map(U256::from)
|
||||
} else if let Some(variable_name) = item.strip_prefix(Self::VARIABLE_PREFIX) {
|
||||
context
|
||||
.variable(variable_name)
|
||||
.context("Variable lookup failed")
|
||||
.copied()
|
||||
context.variable(variable_name).context("Variable lookup failed").copied()
|
||||
} else {
|
||||
U256::from_str_radix(item, 10)
|
||||
.map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))
|
||||
};
|
||||
value.map(CalldataToken::Item)
|
||||
}
|
||||
},
|
||||
Self::Operation(operation) => Ok(CalldataToken::Operation(operation)),
|
||||
}
|
||||
}
|
||||
@@ -959,9 +941,12 @@ impl<'de> Deserialize<'de> for EtherValue {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use alloy::primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address};
|
||||
use alloy::sol_types::SolValue;
|
||||
use alloy::{eips::BlockNumberOrTag, json_abi::JsonAbi};
|
||||
use alloy::{
|
||||
eips::BlockNumberOrTag,
|
||||
json_abi::JsonAbi,
|
||||
primitives::{BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, address},
|
||||
sol_types::SolValue,
|
||||
};
|
||||
use std::{collections::HashMap, pin::Pin};
|
||||
|
||||
use super::*;
|
||||
@@ -1045,13 +1030,7 @@ mod tests {
|
||||
"#;
|
||||
|
||||
let parsed_abi: JsonAbi = serde_json::from_str(raw_metadata).unwrap();
|
||||
let selector = parsed_abi
|
||||
.function("store")
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap()
|
||||
.selector()
|
||||
.0;
|
||||
let selector = parsed_abi.function("store").unwrap().first().unwrap().selector().0;
|
||||
|
||||
let input = FunctionCallStep {
|
||||
instance: ContractInstance::new("Contract"),
|
||||
@@ -1089,13 +1068,7 @@ mod tests {
|
||||
]"#;
|
||||
|
||||
let parsed_abi: JsonAbi = serde_json::from_str(raw_abi).unwrap();
|
||||
let selector = parsed_abi
|
||||
.function("send")
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap()
|
||||
.selector()
|
||||
.0;
|
||||
let selector = parsed_abi.function("send").unwrap().first().unwrap().selector().0;
|
||||
|
||||
let input: FunctionCallStep = FunctionCallStep {
|
||||
instance: "Contract".to_owned().into(),
|
||||
@@ -1117,10 +1090,7 @@ mod tests {
|
||||
|
||||
type T = (alloy::primitives::Address,);
|
||||
let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap();
|
||||
assert_eq!(
|
||||
decoded.0,
|
||||
address!("0x1000000000000000000000000000000000000001")
|
||||
);
|
||||
assert_eq!(decoded.0, address!("0x1000000000000000000000000000000000000001"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1136,13 +1106,7 @@ mod tests {
|
||||
]"#;
|
||||
|
||||
let parsed_abi: JsonAbi = serde_json::from_str(raw_abi).unwrap();
|
||||
let selector = parsed_abi
|
||||
.function("send")
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap()
|
||||
.selector()
|
||||
.0;
|
||||
let selector = parsed_abi.function("send").unwrap().first().unwrap().selector().0;
|
||||
|
||||
let input: FunctionCallStep = FunctionCallStep {
|
||||
instance: ContractInstance::new("Contract"),
|
||||
@@ -1164,10 +1128,7 @@ mod tests {
|
||||
|
||||
type T = (alloy::primitives::Address,);
|
||||
let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap();
|
||||
assert_eq!(
|
||||
decoded.0,
|
||||
address!("0x1000000000000000000000000000000000000001")
|
||||
);
|
||||
assert_eq!(decoded.0, address!("0x1000000000000000000000000000000000000001"));
|
||||
}
|
||||
|
||||
async fn resolve_calldata_item(
|
||||
@@ -1204,12 +1165,7 @@ mod tests {
|
||||
let resolved = resolved.expect("Failed to resolve argument");
|
||||
assert_eq!(
|
||||
resolved,
|
||||
U256::from(
|
||||
MockResolver
|
||||
.block_gas_limit(Default::default())
|
||||
.await
|
||||
.unwrap()
|
||||
)
|
||||
U256::from(MockResolver.block_gas_limit(Default::default()).await.unwrap())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1226,11 +1182,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
resolved,
|
||||
U256::from_be_slice(
|
||||
MockResolver
|
||||
.block_coinbase(Default::default())
|
||||
.await
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
MockResolver.block_coinbase(Default::default()).await.unwrap().as_ref()
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1245,13 +1197,7 @@ mod tests {
|
||||
|
||||
// Assert
|
||||
let resolved = resolved.expect("Failed to resolve argument");
|
||||
assert_eq!(
|
||||
resolved,
|
||||
MockResolver
|
||||
.block_difficulty(Default::default())
|
||||
.await
|
||||
.unwrap()
|
||||
)
|
||||
assert_eq!(resolved, MockResolver.block_difficulty(Default::default()).await.unwrap())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1266,11 +1212,7 @@ mod tests {
|
||||
let resolved = resolved.expect("Failed to resolve argument");
|
||||
assert_eq!(
|
||||
resolved,
|
||||
MockResolver
|
||||
.block_base_fee(Default::default())
|
||||
.await
|
||||
.map(U256::from)
|
||||
.unwrap()
|
||||
MockResolver.block_base_fee(Default::default()).await.map(U256::from).unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1300,10 +1242,7 @@ mod tests {
|
||||
|
||||
// Assert
|
||||
let resolved = resolved.expect("Failed to resolve argument");
|
||||
assert_eq!(
|
||||
resolved,
|
||||
U256::from(MockResolver.last_block_number().await.unwrap())
|
||||
)
|
||||
assert_eq!(resolved, U256::from(MockResolver.last_block_number().await.unwrap()))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1318,12 +1257,7 @@ mod tests {
|
||||
let resolved = resolved.expect("Failed to resolve argument");
|
||||
assert_eq!(
|
||||
resolved,
|
||||
U256::from(
|
||||
MockResolver
|
||||
.block_timestamp(Default::default())
|
||||
.await
|
||||
.unwrap()
|
||||
)
|
||||
U256::from(MockResolver.block_timestamp(Default::default()).await.unwrap())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1401,10 +1335,7 @@ mod tests {
|
||||
|
||||
// Assert
|
||||
let resolved = resolved.expect("Failed to resolve argument");
|
||||
assert_eq!(
|
||||
resolved,
|
||||
U256::from(MockResolver.last_block_number().await.unwrap() + 10)
|
||||
);
|
||||
assert_eq!(resolved, U256::from(MockResolver.last_block_number().await.unwrap() + 10));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
+12
-13
@@ -1,10 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
use std::{collections::HashMap, pin::Pin};
|
||||
|
||||
use alloy::eips::BlockNumberOrTag;
|
||||
use alloy::json_abi::JsonAbi;
|
||||
use alloy::primitives::TxHash;
|
||||
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
|
||||
use alloy::{
|
||||
eips::BlockNumberOrTag,
|
||||
json_abi::JsonAbi,
|
||||
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, TxHash, U256},
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::metadata::{ContractIdent, ContractInstance};
|
||||
@@ -135,11 +135,11 @@ impl<'a> ResolutionContext<'a> {
|
||||
match self.block_number {
|
||||
Some(block_number) => match number {
|
||||
BlockNumberOrTag::Latest => BlockNumberOrTag::Number(*block_number),
|
||||
n @ (BlockNumberOrTag::Finalized
|
||||
| BlockNumberOrTag::Safe
|
||||
| BlockNumberOrTag::Earliest
|
||||
| BlockNumberOrTag::Pending
|
||||
| BlockNumberOrTag::Number(_)) => n,
|
||||
n @ (BlockNumberOrTag::Finalized |
|
||||
BlockNumberOrTag::Safe |
|
||||
BlockNumberOrTag::Earliest |
|
||||
BlockNumberOrTag::Pending |
|
||||
BlockNumberOrTag::Number(_)) => n,
|
||||
},
|
||||
None => number,
|
||||
}
|
||||
@@ -162,8 +162,7 @@ impl<'a> ResolutionContext<'a> {
|
||||
}
|
||||
|
||||
pub fn variable(&self, name: impl AsRef<str>) -> Option<&U256> {
|
||||
self.variables
|
||||
.and_then(|variables| variables.get(name.as_ref()))
|
||||
self.variables.and_then(|variables| variables.get(name.as_ref()))
|
||||
}
|
||||
|
||||
pub fn tip_block_number(&self) -> Option<&'a BlockNumber> {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "ml-test-runner"
|
||||
description = "ML-based test runner for executing differential tests file by file"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "ml-test-runner"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
revive-dt-common = { workspace = true }
|
||||
revive-dt-compiler = { workspace = true }
|
||||
revive-dt-config = { workspace = true }
|
||||
revive-dt-core = { workspace = true }
|
||||
revive-dt-format = { workspace = true }
|
||||
revive-dt-node = { workspace = true }
|
||||
revive-dt-node-interaction = { workspace = true }
|
||||
revive-dt-report = { workspace = true }
|
||||
|
||||
alloy = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
temp-dir = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,74 @@
|
||||
# ML Test Runner
|
||||
|
||||
A test runner for executing Revive differential tests file-by-file with cargo-test-style output.
|
||||
|
||||
This is similar to the `retester` binary but designed for ML-based test execution with a focus on:
|
||||
- Running tests file-by-file (rather than in bulk)
|
||||
- Caching passed tests to skip them in future runs
|
||||
- Providing cargo-test-style output for easy integration with ML pipelines
|
||||
- Single platform testing (rather than differential testing)
|
||||
|
||||
## Features
|
||||
|
||||
- **File-by-file execution**: Run tests on individual `.sol` files, corpus files (`.json`), or recursively walk directories
|
||||
- **Cached results**: Skip tests that have already passed using `--cached-passed`
|
||||
- **Fail fast**: Stop on first failure with `--bail`
|
||||
- **Cargo-like output**: Familiar test output format with colored pass/fail indicators
|
||||
- **Platform support**: Test against `geth` or `kitchensink` platforms
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Run a single .sol file (compile-only mode, default)
|
||||
./ml-test-runner path/to/test.sol --platform geth
|
||||
|
||||
# Run all tests in a corpus file
|
||||
./ml-test-runner path/to/corpus.json --platform kitchensink
|
||||
|
||||
# Walk a directory recursively for .sol files
|
||||
./ml-test-runner path/to/tests/ --platform geth
|
||||
|
||||
# Use cached results and bail on first failure
|
||||
./ml-test-runner path/to/tests/ --cached-passed ./cache.txt --bail
|
||||
|
||||
# Start the platform and execute tests (full mode)
|
||||
./ml-test-runner path/to/tests/ --platform geth --start-platform
|
||||
|
||||
# Enable verbose logging (info, debug, or trace level)
|
||||
RUST_LOG=info ./ml-test-runner path/to/tests/
|
||||
RUST_LOG=debug ./ml-test-runner path/to/tests/ --start-platform
|
||||
RUST_LOG=trace ./ml-test-runner path/to/tests/ --start-platform
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
- `<PATH>` - Path to test file (`.sol`), corpus file (`.json`), or folder of `.sol` files
|
||||
- `--cached-passed <FILE>` - File to track tests that have already passed
|
||||
- `--bail` - Stop after the first file failure
|
||||
- `--platform <PLATFORM>` - Platform to test against (`geth`, `kitchensink`, or `zombienet`, default: `geth`)
|
||||
- `--start-platform` - Start the platform and execute tests (default: `false`, compile-only mode)
|
||||
|
||||
## Output Format
|
||||
|
||||
The runner produces cargo-test-style output:
|
||||
|
||||
```
|
||||
test path/to/test1.sol ... ok
|
||||
test path/to/test2.sol ... FAILED
|
||||
test path/to/test3.sol ... cached
|
||||
|
||||
failures:
|
||||
|
||||
---- path/to/test2.sol ----
|
||||
Error: ...
|
||||
|
||||
test result: FAILED. 1 passed; 1 failed; 1 cached; finished in 2.34s
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cargo build --release -p ml-test-runner
|
||||
```
|
||||
|
||||
The binary will be available at `target/release/ml-test-runner`.
|
||||
@@ -0,0 +1,639 @@
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use revive_dt_common::{
|
||||
iterators::FilesWithExtensionIterator,
|
||||
types::{PlatformIdentifier, PrivateKeyAllocator},
|
||||
};
|
||||
use revive_dt_config::TestExecutionContext;
|
||||
use revive_dt_core::{
|
||||
CachedCompiler, Platform,
|
||||
helpers::{TestDefinition, TestPlatformInformation},
|
||||
};
|
||||
use revive_dt_format::{
|
||||
case::CaseIdx,
|
||||
corpus::Corpus,
|
||||
metadata::{Metadata, MetadataFile},
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, HashSet},
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use temp_dir::TempDir;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::{EnvFilter, FmtSubscriber};
|
||||
|
||||
/// ML-based test runner for executing differential tests file by file
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "ml-test-runner")]
|
||||
struct MlTestRunnerArgs {
|
||||
/// Path to test file (.sol), corpus file (.json), or folder containing .sol files
|
||||
#[arg(value_name = "PATH")]
|
||||
path: PathBuf,
|
||||
|
||||
/// File to cache tests that have already passed
|
||||
#[arg(long = "cached-passed")]
|
||||
cached_passed: Option<PathBuf>,
|
||||
|
||||
/// File to store tests that have failed (defaults to .<platform>-failed)
|
||||
#[arg(long = "cached-failed")]
|
||||
cached_failed: Option<PathBuf>,
|
||||
|
||||
/// Stop after the first file failure
|
||||
#[arg(long = "bail")]
|
||||
bail: bool,
|
||||
|
||||
/// Platform to test against (e.g., geth-evm-solc, kitchensink-polkavm-resolc)
|
||||
#[arg(long = "platform", default_value = "geth-evm-solc")]
|
||||
platform: PlatformIdentifier,
|
||||
|
||||
/// Start the platform and wait for RPC readiness
|
||||
#[arg(long = "start-platform", default_value = "false")]
|
||||
start_platform: bool,
|
||||
|
||||
/// Private key to use for wallet initialization (hex string with or without 0x prefix)
|
||||
#[arg(
|
||||
long = "private-key",
|
||||
default_value = "0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133"
|
||||
)]
|
||||
private_key: String,
|
||||
|
||||
/// RPC port to connect to when using existing node
|
||||
#[arg(long = "rpc-port", default_value = "8545")]
|
||||
rpc_port: u16,
|
||||
|
||||
/// Show verbose output including cached tests and detailed error messages
|
||||
#[arg(long = "verbose", short = 'v')]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let args = MlTestRunnerArgs::parse();
|
||||
|
||||
// Only set up tracing if RUST_LOG is explicitly set or --verbose is passed
|
||||
if std::env::var("RUST_LOG").is_ok() || args.verbose {
|
||||
let subscriber = FmtSubscriber::builder()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_writer(std::io::stderr)
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.expect("Failed to set tracing subscriber");
|
||||
}
|
||||
|
||||
info!("ML test runner starting");
|
||||
info!("Platform: {:?}", args.platform);
|
||||
info!("Start platform: {}", args.start_platform);
|
||||
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Failed building the Runtime")
|
||||
.block_on(run(args))
|
||||
}
|
||||
|
||||
/// Wait for HTTP server to be ready by attempting to connect to the specified port
|
||||
async fn wait_for_http_server(port: u16) -> anyhow::Result<()> {
|
||||
const MAX_RETRIES: u32 = 60;
|
||||
const RETRY_DELAY: Duration = Duration::from_secs(1);
|
||||
|
||||
for attempt in 1..=MAX_RETRIES {
|
||||
match tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port)).await {
|
||||
Ok(_) => {
|
||||
info!("Successfully connected to HTTP server on port {} (attempt {})", port, attempt);
|
||||
return Ok(());
|
||||
},
|
||||
Err(e) => {
|
||||
if attempt == MAX_RETRIES {
|
||||
anyhow::bail!(
|
||||
"Failed to connect to HTTP server on port {} after {} attempts: {}",
|
||||
port,
|
||||
MAX_RETRIES,
|
||||
e
|
||||
);
|
||||
}
|
||||
if attempt % 10 == 0 {
|
||||
info!(
|
||||
"Still waiting for HTTP server on port {} (attempt {}/{})",
|
||||
port, attempt, MAX_RETRIES
|
||||
);
|
||||
}
|
||||
tokio::time::sleep(RETRY_DELAY).await;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn run(args: MlTestRunnerArgs) -> anyhow::Result<()> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
info!("Discovering test files from: {}", args.path.display());
|
||||
let test_files = discover_test_files(&args.path)?;
|
||||
info!("Found {} test file(s)", test_files.len());
|
||||
|
||||
let cached_passed = if let Some(cache_file) = &args.cached_passed {
|
||||
let cached = load_cached_passed(cache_file)?;
|
||||
info!("Loaded {} cached passed test(s)", cached.len());
|
||||
cached
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
|
||||
let cached_passed = Arc::new(Mutex::new(cached_passed));
|
||||
|
||||
// Set up cached-failed file (defaults to .<platform>-failed)
|
||||
let cached_failed_path = args
|
||||
.cached_failed
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from(format!(".{:?}-failed", args.platform)));
|
||||
|
||||
let cached_failed = Arc::new(Mutex::new(HashSet::<String>::new()));
|
||||
|
||||
// Get the platform based on CLI args
|
||||
let platform: &dyn Platform = match args.platform {
|
||||
PlatformIdentifier::GethEvmSolc => &revive_dt_core::GethEvmSolcPlatform,
|
||||
PlatformIdentifier::LighthouseGethEvmSolc => &revive_dt_core::LighthouseGethEvmSolcPlatform,
|
||||
PlatformIdentifier::KitchensinkPolkavmResolc =>
|
||||
&revive_dt_core::KitchensinkPolkavmResolcPlatform,
|
||||
PlatformIdentifier::KitchensinkRevmSolc => &revive_dt_core::KitchensinkRevmSolcPlatform,
|
||||
PlatformIdentifier::ReviveDevNodePolkavmResolc =>
|
||||
&revive_dt_core::ReviveDevNodePolkavmResolcPlatform,
|
||||
PlatformIdentifier::ReviveDevNodeRevmSolc => &revive_dt_core::ReviveDevNodeRevmSolcPlatform,
|
||||
PlatformIdentifier::ZombienetPolkavmResolc =>
|
||||
&revive_dt_core::ZombienetPolkavmResolcPlatform,
|
||||
PlatformIdentifier::ZombienetRevmSolc => &revive_dt_core::ZombienetRevmSolcPlatform,
|
||||
};
|
||||
|
||||
let test_context = TestExecutionContext::default();
|
||||
let context = revive_dt_config::Context::Test(Box::new(test_context));
|
||||
|
||||
let node: &'static dyn revive_dt_node_interaction::EthereumNode = if args.start_platform {
|
||||
info!("Starting blockchain node...");
|
||||
let node_handle =
|
||||
platform.new_node(context.clone()).context("Failed to spawn node thread")?;
|
||||
|
||||
info!("Waiting for node to start...");
|
||||
let node = node_handle
|
||||
.join()
|
||||
.map_err(|e| anyhow::anyhow!("Node thread panicked: {:?}", e))?
|
||||
.context("Failed to start node")?;
|
||||
|
||||
info!("Node started with ID: {}, connection: {}", node.id(), node.connection_string());
|
||||
let node = Box::leak(node);
|
||||
|
||||
info!("Running pre-transactions...");
|
||||
node.pre_transactions().await.context("Failed to run pre-transactions")?;
|
||||
info!("Pre-transactions completed");
|
||||
|
||||
node
|
||||
} else {
|
||||
info!("Using existing node at port {}", args.rpc_port);
|
||||
|
||||
// Wait for the HTTP server to be ready
|
||||
info!("Waiting for HTTP server to be ready on port {}...", args.rpc_port);
|
||||
wait_for_http_server(args.rpc_port).await?;
|
||||
info!("HTTP server is ready");
|
||||
|
||||
let existing_node: Box<dyn revive_dt_node_interaction::EthereumNode> = match args.platform {
|
||||
PlatformIdentifier::GethEvmSolc | PlatformIdentifier::LighthouseGethEvmSolc =>
|
||||
Box::new(
|
||||
revive_dt_node::node_implementations::geth::GethNode::new_existing(
|
||||
&args.private_key,
|
||||
args.rpc_port,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
PlatformIdentifier::KitchensinkPolkavmResolc |
|
||||
PlatformIdentifier::KitchensinkRevmSolc |
|
||||
PlatformIdentifier::ReviveDevNodePolkavmResolc |
|
||||
PlatformIdentifier::ReviveDevNodeRevmSolc |
|
||||
PlatformIdentifier::ZombienetPolkavmResolc |
|
||||
PlatformIdentifier::ZombienetRevmSolc => Box::new(
|
||||
revive_dt_node::node_implementations::substrate::SubstrateNode::new_existing(
|
||||
&args.private_key,
|
||||
args.rpc_port,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
};
|
||||
Box::leak(existing_node)
|
||||
};
|
||||
|
||||
let mut passed_files = 0;
|
||||
let mut failed_files = 0;
|
||||
let mut skipped_files = 0;
|
||||
let mut failures = Vec::new();
|
||||
|
||||
const GREEN: &str = "\x1B[32m";
|
||||
const RED: &str = "\x1B[31m";
|
||||
const YELLOW: &str = "\x1B[33m";
|
||||
const COLOUR_RESET: &str = "\x1B[0m";
|
||||
const BOLD: &str = "\x1B[1m";
|
||||
const BOLD_RESET: &str = "\x1B[22m";
|
||||
|
||||
for test_file in test_files {
|
||||
let file_display = test_file.display().to_string();
|
||||
|
||||
info!("\n\n == Executing test file: {file_display} == \n\n");
|
||||
// Check if already passed
|
||||
{
|
||||
let cache = cached_passed.lock().await;
|
||||
if cache.contains(&file_display) {
|
||||
if args.verbose {
|
||||
println!("test {file_display} ... {YELLOW}cached{COLOUR_RESET}");
|
||||
}
|
||||
skipped_files += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
info!("Loading metadata from: {}", test_file.display());
|
||||
let metadata_file = match load_metadata_file(&test_file) {
|
||||
Ok(mf) => {
|
||||
info!("Loaded metadata with {} case(s)", mf.cases.len());
|
||||
mf
|
||||
},
|
||||
Err(e) => {
|
||||
// Skip files without metadata instead of treating them as failures
|
||||
info!("Skipping {} (no metadata): {}", file_display, e);
|
||||
skipped_files += 1;
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
// Execute test with 10 second timeout
|
||||
let test_result = tokio::time::timeout(
|
||||
Duration::from_secs(20),
|
||||
execute_test_file(&metadata_file, platform, node, &context),
|
||||
)
|
||||
.await;
|
||||
|
||||
let result = match test_result {
|
||||
Ok(Ok(_)) => Ok(()),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(_) => Err(anyhow::anyhow!("Test timed out after 20 seconds")),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
println!("test {file_display} ... {GREEN}ok{COLOUR_RESET}");
|
||||
passed_files += 1;
|
||||
|
||||
// Update cache
|
||||
if let Some(cache_file) = &args.cached_passed {
|
||||
let mut cache = cached_passed.lock().await;
|
||||
cache.insert(file_display);
|
||||
if let Err(e) = save_cached_passed(cache_file, &cache) {
|
||||
info!("Failed to save cache: {}", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("test {file_display} ... {RED}FAILED{COLOUR_RESET}");
|
||||
failed_files += 1;
|
||||
let error_detail = if args.verbose { format!("{:?}", e) } else { format!("{}", e) };
|
||||
failures.push((file_display.clone(), error_detail));
|
||||
|
||||
// Update cached-failed
|
||||
{
|
||||
let mut cache = cached_failed.lock().await;
|
||||
cache.insert(file_display);
|
||||
if let Err(e) = save_cached_failed(&cached_failed_path, &cache) {
|
||||
info!("Failed to save cached-failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
if args.bail {
|
||||
info!("Bailing after first failure");
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Print summary
|
||||
println!();
|
||||
if !failures.is_empty() && args.verbose {
|
||||
println!("{BOLD}failures:{BOLD_RESET}");
|
||||
println!();
|
||||
for (file, error) in &failures {
|
||||
println!("---- {} ----", file);
|
||||
println!("{}", error);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
println!(
|
||||
"test result: {}. {} passed; {} failed; {} cached; finished in {:.2}s",
|
||||
if failed_files == 0 {
|
||||
format!("{GREEN}ok{COLOUR_RESET}")
|
||||
} else {
|
||||
format!("{RED}FAILED{COLOUR_RESET}")
|
||||
},
|
||||
passed_files,
|
||||
failed_files,
|
||||
skipped_files,
|
||||
elapsed.as_secs_f64()
|
||||
);
|
||||
|
||||
if failed_files > 0 {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Discover test files from the given path
|
||||
fn discover_test_files(path: &Path) -> anyhow::Result<Vec<PathBuf>> {
|
||||
if !path.exists() {
|
||||
anyhow::bail!("Path does not exist: {}", path.display());
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
|
||||
if path.is_file() {
|
||||
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("");
|
||||
|
||||
match extension {
|
||||
"sol" => {
|
||||
// Single .sol file
|
||||
files.push(path.to_path_buf());
|
||||
},
|
||||
"json" => {
|
||||
// Corpus file - enumerate its tests
|
||||
let corpus = Corpus::try_from_path(path)?;
|
||||
let metadata_files = corpus.enumerate_tests();
|
||||
for metadata in metadata_files {
|
||||
files.push(metadata.metadata_file_path);
|
||||
}
|
||||
},
|
||||
_ => anyhow::bail!("Unsupported file extension: {}. Expected .sol or .json", extension),
|
||||
}
|
||||
} else if path.is_dir() {
|
||||
// First, find all test.json files
|
||||
let mut test_json_dirs = HashSet::new();
|
||||
for json_file in FilesWithExtensionIterator::new(path)
|
||||
.with_allowed_extension("json")
|
||||
.with_use_cached_fs(true)
|
||||
{
|
||||
if json_file.file_name().and_then(|s| s.to_str()) == Some("test.json") {
|
||||
if let Some(parent) = json_file.parent() {
|
||||
test_json_dirs.insert(parent.to_path_buf());
|
||||
}
|
||||
|
||||
// Try to parse as corpus file first, then as metadata file
|
||||
if let Ok(corpus) = Corpus::try_from_path(&json_file) {
|
||||
// It's a corpus file - enumerate its tests
|
||||
let metadata_files = corpus.enumerate_tests();
|
||||
for metadata in metadata_files {
|
||||
files.push(metadata.metadata_file_path);
|
||||
}
|
||||
} else {
|
||||
// It's a metadata file - use it directly
|
||||
files.push(json_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then, find .sol files that are NOT in directories with test.json
|
||||
for sol_file in FilesWithExtensionIterator::new(path)
|
||||
.with_allowed_extension("sol")
|
||||
.with_use_cached_fs(true)
|
||||
{
|
||||
if let Some(parent) = sol_file.parent() {
|
||||
if !test_json_dirs.contains(parent) {
|
||||
files.push(sol_file);
|
||||
}
|
||||
} else {
|
||||
files.push(sol_file);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!("Path is neither a file nor a directory: {}", path.display());
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// Load metadata from a test file
|
||||
fn load_metadata_file(path: &Path) -> anyhow::Result<MetadataFile> {
|
||||
let metadata = Metadata::try_from_file(path)
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to load metadata from {}", path.display()))?;
|
||||
|
||||
Ok(MetadataFile {
|
||||
metadata_file_path: path.to_path_buf(),
|
||||
corpus_file_path: path.to_path_buf(),
|
||||
content: metadata,
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute all test cases in a metadata file
|
||||
async fn execute_test_file(
|
||||
metadata_file: &MetadataFile,
|
||||
platform: &dyn Platform,
|
||||
node: &'static dyn revive_dt_node_interaction::EthereumNode,
|
||||
context: &revive_dt_config::Context,
|
||||
) -> anyhow::Result<()> {
|
||||
if metadata_file.cases.is_empty() {
|
||||
anyhow::bail!("No test cases found in file");
|
||||
}
|
||||
|
||||
info!("Processing {} test case(s)", metadata_file.cases.len());
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
info!("Created temporary directory: {}", temp_dir.path().display());
|
||||
|
||||
info!("Initializing cached compiler");
|
||||
let cached_compiler = CachedCompiler::new(temp_dir.path().join("compilation_cache"), false)
|
||||
.await
|
||||
.map(Arc::new)
|
||||
.context("Failed to create cached compiler")?;
|
||||
|
||||
let private_key_allocator =
|
||||
Arc::new(Mutex::new(PrivateKeyAllocator::new(alloy::primitives::U256::from(100))));
|
||||
|
||||
let (reporter, report_task) =
|
||||
revive_dt_report::ReportAggregator::new(context.clone()).into_task();
|
||||
|
||||
tokio::spawn(report_task);
|
||||
|
||||
info!("Building test definitions for {} case(s)", metadata_file.cases.len());
|
||||
let mut test_definitions = Vec::new();
|
||||
for (case_idx, case) in metadata_file.cases.iter().enumerate() {
|
||||
info!("Building test definition for case {}", case_idx);
|
||||
let test_def = build_test_definition(
|
||||
metadata_file,
|
||||
case,
|
||||
case_idx,
|
||||
platform,
|
||||
node,
|
||||
&context,
|
||||
&reporter,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(test_def) = test_def {
|
||||
info!("Test definition for case {} created successfully", case_idx);
|
||||
test_definitions.push(test_def);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Executing {} test definition(s)", test_definitions.len());
|
||||
for (idx, test_definition) in test_definitions.iter().enumerate() {
|
||||
info!("─────────────────────────────────────────────────────────────────");
|
||||
info!(
|
||||
"Executing case {}/{}: case_idx={}, mode={}, steps={}",
|
||||
idx + 1,
|
||||
test_definitions.len(),
|
||||
test_definition.case_idx,
|
||||
test_definition.mode,
|
||||
test_definition.case.steps.len()
|
||||
);
|
||||
|
||||
info!("Creating driver for case {}", test_definition.case_idx);
|
||||
let driver = revive_dt_core::differential_tests::Driver::new_root(
|
||||
test_definition,
|
||||
private_key_allocator.clone(),
|
||||
&cached_compiler,
|
||||
)
|
||||
.await
|
||||
.context("Failed to create driver")?;
|
||||
|
||||
info!(
|
||||
"Running {} step(s) for case {}",
|
||||
test_definition.case.steps.len(),
|
||||
test_definition.case_idx
|
||||
);
|
||||
let steps_executed = driver
|
||||
.execute_all()
|
||||
.await
|
||||
.context(format!("Failed to execute case {}", test_definition.case_idx))?;
|
||||
info!(
|
||||
"✓ Case {} completed successfully, executed {} step(s)",
|
||||
test_definition.case_idx, steps_executed
|
||||
);
|
||||
}
|
||||
info!("─────────────────────────────────────────────────────────────────");
|
||||
info!("All {} test case(s) executed successfully", test_definitions.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build a test definition for a single test case
|
||||
async fn build_test_definition<'a>(
|
||||
metadata_file: &'a MetadataFile,
|
||||
case: &'a revive_dt_format::case::Case,
|
||||
case_idx: usize,
|
||||
platform: &'a dyn Platform,
|
||||
node: &'a dyn revive_dt_node_interaction::EthereumNode,
|
||||
context: &revive_dt_config::Context,
|
||||
reporter: &revive_dt_report::Reporter,
|
||||
) -> anyhow::Result<Option<TestDefinition<'a>>> {
|
||||
let mode = case
|
||||
.modes
|
||||
.as_ref()
|
||||
.or(metadata_file.modes.as_ref())
|
||||
.and_then(|modes| modes.first())
|
||||
.and_then(|parsed_mode| parsed_mode.to_modes().next())
|
||||
.map(Cow::Owned)
|
||||
.or_else(|| revive_dt_compiler::Mode::all().next().map(Cow::Borrowed))
|
||||
.unwrap();
|
||||
|
||||
let compiler = platform
|
||||
.new_compiler(context.clone(), mode.version.clone().map(Into::into))
|
||||
.await
|
||||
.context("Failed to create compiler")?;
|
||||
|
||||
let test_reporter =
|
||||
reporter.test_specific_reporter(Arc::new(revive_dt_report::TestSpecifier {
|
||||
solc_mode: mode.as_ref().clone(),
|
||||
metadata_file_path: metadata_file.metadata_file_path.clone(),
|
||||
case_idx: CaseIdx::new(case_idx),
|
||||
}));
|
||||
|
||||
let execution_reporter =
|
||||
test_reporter.execution_specific_reporter(node.id(), platform.platform_identifier());
|
||||
|
||||
let mut platforms = BTreeMap::new();
|
||||
platforms.insert(
|
||||
platform.platform_identifier(),
|
||||
TestPlatformInformation { platform, node, compiler, reporter: execution_reporter },
|
||||
);
|
||||
|
||||
let test_definition = TestDefinition {
|
||||
metadata: metadata_file,
|
||||
metadata_file_path: &metadata_file.metadata_file_path,
|
||||
mode,
|
||||
case_idx: CaseIdx::new(case_idx),
|
||||
case,
|
||||
platforms,
|
||||
reporter: test_reporter,
|
||||
};
|
||||
|
||||
if let Err((reason, _)) = test_definition.check_compatibility() {
|
||||
info!("Skipping case {}: {}", case_idx, reason);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(test_definition))
|
||||
}
|
||||
|
||||
/// Load cached passed tests from file
|
||||
fn load_cached_passed(path: &Path) -> anyhow::Result<HashSet<String>> {
|
||||
if !path.exists() {
|
||||
return Ok(HashSet::new());
|
||||
}
|
||||
|
||||
let file = File::open(path).context("Failed to open cached-passed file")?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let mut cache = HashSet::new();
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty() {
|
||||
cache.insert(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cache)
|
||||
}
|
||||
|
||||
/// Save cached passed tests to file
|
||||
fn save_cached_passed(path: &Path, cache: &HashSet<String>) -> anyhow::Result<()> {
|
||||
let file = File::create(path).context("Failed to create cached-passed file")?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
let mut entries: Vec<_> = cache.iter().collect();
|
||||
entries.sort();
|
||||
|
||||
for entry in entries {
|
||||
writeln!(writer, "{}", entry)?;
|
||||
}
|
||||
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save cached failed tests to file
|
||||
fn save_cached_failed(path: &Path, cache: &HashSet<String>) -> anyhow::Result<()> {
|
||||
let file = File::create(path).context("Failed to create cached-failed file")?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
let mut entries: Vec<_> = cache.iter().collect();
|
||||
entries.sort();
|
||||
|
||||
for entry in entries {
|
||||
writeln!(writer, "{}", entry)?;
|
||||
}
|
||||
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
//! This crate implements all node interactions.
|
||||
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::{pin::Pin, sync::Arc};
|
||||
|
||||
use alloy::primitives::{Address, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256};
|
||||
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
|
||||
use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest};
|
||||
use alloy::{
|
||||
primitives::{Address, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
|
||||
rpc::types::{
|
||||
EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
|
||||
trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace},
|
||||
},
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
||||
use futures::Stream;
|
||||
@@ -74,6 +77,10 @@ pub trait EthereumNode {
|
||||
+ '_,
|
||||
>,
|
||||
>;
|
||||
|
||||
/// Checks if the provided address is in the wallet. If it is, returns the address.
|
||||
/// Otherwise, returns the default signer's address.
|
||||
fn resolve_signer_or_default(&self, address: Address) -> Address;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
|
||||
@@ -11,7 +11,6 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
alloy = { workspace = true }
|
||||
async-stream = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tower = { workspace = true }
|
||||
|
||||
@@ -33,10 +33,7 @@ impl Process {
|
||||
let log_file_prefix = log_file_prefix.into();
|
||||
|
||||
let (stdout_file_name, stderr_file_name) = match log_file_prefix {
|
||||
Some(prefix) => (
|
||||
format!("{prefix}_stdout.log"),
|
||||
format!("{prefix}_stderr.log"),
|
||||
),
|
||||
Some(prefix) => (format!("{prefix}_stdout.log"), format!("{prefix}_stderr.log")),
|
||||
None => ("stdout.log".to_string(), "stderr.log".to_string()),
|
||||
};
|
||||
|
||||
@@ -57,23 +54,19 @@ impl Process {
|
||||
.context("Failed to open the stderr logs file")?;
|
||||
|
||||
let mut command = {
|
||||
let stdout_logs_file = stdout_logs_file
|
||||
.try_clone()
|
||||
.context("Failed to clone the stdout logs file")?;
|
||||
let stderr_logs_file = stderr_logs_file
|
||||
.try_clone()
|
||||
.context("Failed to clone the stderr logs file")?;
|
||||
let stdout_logs_file =
|
||||
stdout_logs_file.try_clone().context("Failed to clone the stdout logs file")?;
|
||||
let stderr_logs_file =
|
||||
stderr_logs_file.try_clone().context("Failed to clone the stderr logs file")?;
|
||||
|
||||
let mut command = Command::new(binary_path.as_ref());
|
||||
command_building_callback(&mut command, stdout_logs_file, stderr_logs_file);
|
||||
command
|
||||
};
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.context("Failed to spawn the built command")?;
|
||||
let mut child = command.spawn().context("Failed to spawn the built command")?;
|
||||
|
||||
match process_readiness_wait_behavior {
|
||||
ProcessReadinessWaitBehavior::NoStartupWait => {}
|
||||
ProcessReadinessWaitBehavior::NoStartupWait => {},
|
||||
ProcessReadinessWaitBehavior::WaitDuration(duration) => std::thread::sleep(duration),
|
||||
ProcessReadinessWaitBehavior::TimeBoundedWaitFunction {
|
||||
max_wait_duration,
|
||||
@@ -126,35 +119,23 @@ impl Process {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ProcessReadinessWaitBehavior::WaitForCommandToExit => {
|
||||
if !child
|
||||
.wait()
|
||||
.context("Failed waiting for process to finish")?
|
||||
.success()
|
||||
{
|
||||
if !child.wait().context("Failed waiting for process to finish")?.success() {
|
||||
anyhow::bail!("Failed to spawn command");
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
child,
|
||||
stdout_logs_file,
|
||||
stderr_logs_file,
|
||||
})
|
||||
Ok(Self { child, stdout_logs_file, stderr_logs_file })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Process {
|
||||
fn drop(&mut self) {
|
||||
self.child.kill().expect("Failed to kill the process");
|
||||
self.stdout_logs_file
|
||||
.flush()
|
||||
.expect("Failed to flush the stdout logs file");
|
||||
self.stderr_logs_file
|
||||
.flush()
|
||||
.expect("Failed to flush the stderr logs file");
|
||||
self.stdout_logs_file.flush().expect("Failed to flush the stdout logs file");
|
||||
self.stderr_logs_file.flush().expect("Failed to flush the stderr logs file");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
use alloy::{
|
||||
genesis::{Genesis, GenesisAccount},
|
||||
network::{Ethereum, NetworkWallet},
|
||||
primitives::{Address, U256},
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use serde_json::{Value as JsonValue, json};
|
||||
use sp_core::crypto::Ss58Codec;
|
||||
use sp_runtime::AccountId32;
|
||||
use std::{fs::File, path::Path, process::Command};
|
||||
|
||||
pub fn export_and_patch_chainspec_json(
|
||||
binary_path: &Path,
|
||||
export_command: &str,
|
||||
chain_arg: &str,
|
||||
output_path: &Path,
|
||||
genesis: &mut Genesis,
|
||||
wallet: &impl NetworkWallet<Ethereum>,
|
||||
initial_balance: u128,
|
||||
) -> Result<()> {
|
||||
// Note: we do not pipe the logs of this process to a separate file since this is just a
|
||||
// once-off export of the default chain spec and not part of the long-running node process.
|
||||
let output = Command::new(binary_path)
|
||||
.arg(export_command)
|
||||
.arg("--chain")
|
||||
.arg(chain_arg)
|
||||
.env_remove("RUST_LOG")
|
||||
.output()
|
||||
.context("Failed to export the chain-spec")?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!(
|
||||
"Export chain-spec failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
let content =
|
||||
String::from_utf8(output.stdout).context("Failed to decode chain-spec output as UTF-8")?;
|
||||
let mut chainspec_json: JsonValue =
|
||||
serde_json::from_str(&content).context("Failed to parse chain spec JSON")?;
|
||||
|
||||
let existing_chainspec_balances =
|
||||
chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"]
|
||||
.as_array()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut merged_balances: Vec<(String, u128)> = existing_chainspec_balances
|
||||
.into_iter()
|
||||
.filter_map(|val| {
|
||||
if let Some(arr) = val.as_array() {
|
||||
if arr.len() == 2 {
|
||||
let account = arr[0].as_str()?.to_string();
|
||||
let balance = arr[1].as_f64()? as u128;
|
||||
return Some((account, balance));
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
for signer_address in wallet.signer_addresses() {
|
||||
genesis
|
||||
.alloc
|
||||
.entry(signer_address)
|
||||
.or_insert(GenesisAccount::default().with_balance(U256::from(initial_balance)));
|
||||
}
|
||||
|
||||
let mut eth_balances =
|
||||
crate::node_implementations::common::chainspec::extract_balance_from_genesis_file(genesis)
|
||||
.context("Failed to extract balances from EVM genesis JSON")?;
|
||||
|
||||
merged_balances.append(&mut eth_balances);
|
||||
|
||||
chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] =
|
||||
json!(merged_balances);
|
||||
|
||||
serde_json::to_writer_pretty(
|
||||
File::create(output_path).context("Failed to create chainspec file")?,
|
||||
&chainspec_json,
|
||||
)
|
||||
.context("Failed to write chainspec JSON")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn extract_balance_from_genesis_file(genesis: &Genesis) -> anyhow::Result<Vec<(String, u128)>> {
|
||||
genesis
|
||||
.alloc
|
||||
.iter()
|
||||
.try_fold(Vec::new(), |mut vec, (address, acc)| {
|
||||
let substrate_address = eth_to_polkadot_address(address);
|
||||
let balance = acc.balance.try_into()?;
|
||||
vec.push((substrate_address, balance));
|
||||
Ok(vec)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn eth_to_polkadot_address(address: &Address) -> String {
|
||||
let eth_bytes = address.0.0;
|
||||
|
||||
let mut padded = [0xEEu8; 32];
|
||||
padded[..20].copy_from_slice(ð_bytes);
|
||||
|
||||
let account_id = AccountId32::from(padded);
|
||||
account_id.to_ss58check()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_genesis_alloc() {
|
||||
// Create test genesis file
|
||||
let genesis_json = r#"
|
||||
{
|
||||
"alloc": {
|
||||
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1": { "balance": "1000000000000000000" },
|
||||
"0x0000000000000000000000000000000000000000": { "balance": "0xDE0B6B3A7640000" },
|
||||
"0xffffffffffffffffffffffffffffffffffffffff": { "balance": "123456789" }
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let result =
|
||||
extract_balance_from_genesis_file(&serde_json::from_str(genesis_json).unwrap())
|
||||
.unwrap();
|
||||
|
||||
let result_map: std::collections::HashMap<_, _> = result.into_iter().collect();
|
||||
|
||||
assert_eq!(
|
||||
result_map.get("5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV"),
|
||||
Some(&1_000_000_000_000_000_000u128)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
result_map.get("5C4hrfjw9DjXZTzV3MwzrrAr9P1MLDHajjSidz9bR544LEq1"),
|
||||
Some(&1_000_000_000_000_000_000u128)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
result_map.get("5HrN7fHLXWcFiXPwwtq2EkSGns9eMmoUQnbVKweNz3VVr6N4"),
|
||||
Some(&123_456_789u128)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eth_to_polkadot_address() {
|
||||
let cases = vec![
|
||||
(
|
||||
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
|
||||
"5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV",
|
||||
),
|
||||
(
|
||||
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
|
||||
"5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV",
|
||||
),
|
||||
(
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
"5C4hrfjw9DjXZTzV3MwzrrAr9P1MLDHajjSidz9bR544LEq1",
|
||||
),
|
||||
(
|
||||
"0xffffffffffffffffffffffffffffffffffffffff",
|
||||
"5HrN7fHLXWcFiXPwwtq2EkSGns9eMmoUQnbVKweNz3VVr6N4",
|
||||
),
|
||||
];
|
||||
|
||||
for (eth_addr, expected_ss58) in cases {
|
||||
let result = eth_to_polkadot_address(ð_addr.parse().unwrap());
|
||||
assert_eq!(
|
||||
result, expected_ss58,
|
||||
"Mismatch for Ethereum address {eth_addr}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_eth_to_polkadot_mappings() {
|
||||
let eth_addresses = vec![
|
||||
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
|
||||
"0xffffffffffffffffffffffffffffffffffffffff",
|
||||
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
|
||||
];
|
||||
|
||||
for eth_addr in eth_addresses {
|
||||
let ss58 = eth_to_polkadot_address(ð_addr.parse().unwrap());
|
||||
|
||||
println!("Ethereum: {eth_addr} -> Polkadot SS58: {ss58}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub(crate) mod chainspec;
|
||||
pub(crate) mod process;
|
||||
pub(crate) mod revive;
|
||||
@@ -1,60 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use std::{
|
||||
process::{Command, Stdio},
|
||||
{path::Path, time::Duration},
|
||||
};
|
||||
|
||||
use crate::helpers::{Process, ProcessReadinessWaitBehavior};
|
||||
|
||||
const PROXY_LOG_ENV: &str = "info,eth-rpc=debug";
|
||||
|
||||
pub fn spawn_eth_rpc_process(
|
||||
logs_directory: &Path,
|
||||
eth_proxy_binary: &Path,
|
||||
node_rpc_url: &str,
|
||||
eth_rpc_port: u16,
|
||||
extra_args: &[&str],
|
||||
ready_marker: &str,
|
||||
) -> anyhow::Result<Process> {
|
||||
let ready_marker = ready_marker.to_owned();
|
||||
Process::new(
|
||||
"proxy",
|
||||
logs_directory,
|
||||
eth_proxy_binary,
|
||||
|command, stdout_file, stderr_file| {
|
||||
command
|
||||
.arg("--node-rpc-url")
|
||||
.arg(node_rpc_url)
|
||||
.arg("--rpc-cors")
|
||||
.arg("all")
|
||||
.arg("--rpc-max-connections")
|
||||
.arg(u32::MAX.to_string())
|
||||
.arg("--rpc-port")
|
||||
.arg(eth_rpc_port.to_string())
|
||||
.env("RUST_LOG", PROXY_LOG_ENV);
|
||||
for arg in extra_args {
|
||||
command.arg(arg);
|
||||
}
|
||||
command.stdout(stdout_file).stderr(stderr_file);
|
||||
},
|
||||
ProcessReadinessWaitBehavior::TimeBoundedWaitFunction {
|
||||
max_wait_duration: Duration::from_secs(30),
|
||||
check_function: Box::new(move |_, stderr_line| match stderr_line {
|
||||
Some(line) => Ok(line.contains(&ready_marker)),
|
||||
None => Ok(false),
|
||||
}),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn command_version(command_binary: &Path) -> Result<String> {
|
||||
let output = Command::new(command_binary)
|
||||
.arg("--version")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?
|
||||
.wait_with_output()?
|
||||
.stdout;
|
||||
Ok(String::from_utf8_lossy(&output).trim().to_string())
|
||||
}
|
||||
@@ -1,434 +0,0 @@
|
||||
use alloy::{
|
||||
consensus::{BlockHeader, TxEnvelope},
|
||||
network::{
|
||||
Ethereum, Network, TransactionBuilder, TransactionBuilderError, UnbuiltTransactionError,
|
||||
},
|
||||
primitives::{Address, B64, B256, BlockNumber, Bloom, Bytes, U256},
|
||||
rpc::types::eth::{Block, Header, Transaction},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,9 @@ use alloy::{
|
||||
eips::BlockNumberOrTag,
|
||||
genesis::{Genesis, GenesisAccount},
|
||||
network::{Ethereum, EthereumWallet, NetworkWallet},
|
||||
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
|
||||
primitives::{
|
||||
Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, StorageKey, TxHash, U256,
|
||||
},
|
||||
providers::{
|
||||
Provider,
|
||||
ext::DebugApi,
|
||||
@@ -75,6 +77,7 @@ pub struct GethNode {
|
||||
wallet: Arc<EthereumWallet>,
|
||||
nonce_manager: CachedNonceManager,
|
||||
provider: OnceCell<ConcreteProvider<Ethereum, Arc<EthereumWallet>>>,
|
||||
chain_id: ChainId,
|
||||
}
|
||||
|
||||
impl GethNode {
|
||||
@@ -91,8 +94,8 @@ impl GethNode {
|
||||
const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress";
|
||||
const TRANSACTION_TRACING_ERROR: &str = "historical state not available in path scheme yet";
|
||||
|
||||
const RECEIPT_POLLING_DURATION: Duration = Duration::from_secs(5 * 60);
|
||||
const TRACE_POLLING_DURATION: Duration = Duration::from_secs(60);
|
||||
const RECEIPT_POLLING_DURATION: Duration = Duration::from_secs(10);
|
||||
const TRACE_POLLING_DURATION: Duration = Duration::from_secs(10);
|
||||
|
||||
pub fn new(
|
||||
context: impl AsRef<WorkingDirectoryConfiguration>
|
||||
@@ -105,9 +108,7 @@ impl GethNode {
|
||||
let wallet_configuration = AsRef::<WalletConfiguration>::as_ref(&context);
|
||||
let geth_configuration = AsRef::<GethConfiguration>::as_ref(&context);
|
||||
|
||||
let geth_directory = working_directory_configuration
|
||||
.as_path()
|
||||
.join(Self::BASE_DIRECTORY);
|
||||
let geth_directory = working_directory_configuration.as_path().join(Self::BASE_DIRECTORY);
|
||||
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
let base_directory = geth_directory.join(id.to_string());
|
||||
|
||||
@@ -125,9 +126,117 @@ impl GethNode {
|
||||
wallet: wallet.clone(),
|
||||
nonce_manager: Default::default(),
|
||||
provider: Default::default(),
|
||||
chain_id: CHAIN_ID,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_existing(private_key: &str, rpc_port: u16) -> anyhow::Result<Self> {
|
||||
use alloy::{
|
||||
primitives::FixedBytes,
|
||||
providers::{Provider, ProviderBuilder},
|
||||
signers::local::PrivateKeySigner,
|
||||
};
|
||||
|
||||
let key_str = private_key.trim().strip_prefix("0x").unwrap_or(private_key.trim());
|
||||
let key_bytes = alloy::hex::decode(key_str)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to decode private key hex: {}", e))?;
|
||||
|
||||
if key_bytes.len() != 32 {
|
||||
anyhow::bail!(
|
||||
"Private key must be 32 bytes (64 hex characters), got {}",
|
||||
key_bytes.len()
|
||||
);
|
||||
}
|
||||
|
||||
let mut bytes = [0u8; 32];
|
||||
bytes.copy_from_slice(&key_bytes);
|
||||
|
||||
let signer = PrivateKeySigner::from_bytes(&FixedBytes(bytes))
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create signer from private key: {}", e))?;
|
||||
|
||||
let address = signer.address();
|
||||
let wallet = Arc::new(EthereumWallet::new(signer));
|
||||
let connection_string = format!("http://localhost:{}", rpc_port);
|
||||
|
||||
let chain_id = ProviderBuilder::new()
|
||||
.connect_http(connection_string.parse()?)
|
||||
.get_chain_id()
|
||||
.await
|
||||
.context("Failed to query chain ID from RPC")?;
|
||||
|
||||
let node = Self {
|
||||
connection_string: format!("http://localhost:{}", rpc_port),
|
||||
base_directory: PathBuf::new(),
|
||||
data_directory: PathBuf::new(),
|
||||
logs_directory: PathBuf::new(),
|
||||
geth: PathBuf::new(),
|
||||
id: 0,
|
||||
chain_id,
|
||||
handle: None,
|
||||
start_timeout: Duration::from_secs(0),
|
||||
wallet,
|
||||
nonce_manager: Default::default(),
|
||||
provider: Default::default(),
|
||||
};
|
||||
|
||||
// Check balance and fund if needed
|
||||
node.ensure_funded(address).await?;
|
||||
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
/// Ensure that the given address has at least 1000 ETH, funding it from the node's managed
|
||||
/// account if necessary.
|
||||
async fn ensure_funded(&self, address: Address) -> anyhow::Result<()> {
|
||||
use alloy::{
|
||||
primitives::utils::{format_ether, parse_ether},
|
||||
providers::{Provider, ProviderBuilder},
|
||||
};
|
||||
|
||||
let provider = ProviderBuilder::new().connect_http(self.connection_string.parse()?);
|
||||
let balance = provider.get_balance(address).await?;
|
||||
let min_balance = parse_ether("1000")?;
|
||||
|
||||
if balance >= min_balance {
|
||||
tracing::info!(
|
||||
"Wallet {} already has sufficient balance: {} ETH",
|
||||
address,
|
||||
format_ether(balance)
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Funding wallet {} (current: {} ETH, target: 1000 ETH)",
|
||||
address,
|
||||
format_ether(balance)
|
||||
);
|
||||
|
||||
// Get the node's managed account
|
||||
let accounts = provider.get_accounts().await?;
|
||||
if accounts.is_empty() {
|
||||
anyhow::bail!("No managed accounts available on the node to fund wallet");
|
||||
}
|
||||
|
||||
let from_account = accounts[0];
|
||||
|
||||
let funding_amount = min_balance - balance;
|
||||
let tx = TransactionRequest::default()
|
||||
.from(from_account)
|
||||
.to(address)
|
||||
.value(funding_amount);
|
||||
|
||||
provider
|
||||
.send_transaction(tx)
|
||||
.await?
|
||||
.get_receipt()
|
||||
.await
|
||||
.context("Failed to get receipt for funding transaction")?;
|
||||
|
||||
tracing::info!("Successfully funded wallet {}", address);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create the node directory and call `geth init` to configure the genesis.
|
||||
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
|
||||
fn init(&mut self, mut genesis: Genesis) -> anyhow::Result<&mut Self> {
|
||||
@@ -222,15 +331,14 @@ impl GethNode {
|
||||
ProcessReadinessWaitBehavior::TimeBoundedWaitFunction {
|
||||
max_wait_duration: self.start_timeout,
|
||||
check_function: Box::new(|_, stderr_line| match stderr_line {
|
||||
Some(line) => {
|
||||
Some(line) =>
|
||||
if line.contains(Self::ERROR_MARKER) {
|
||||
anyhow::bail!("Failed to start geth {line}");
|
||||
} else if line.contains(Self::READY_MARKER) {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
None => Ok(false),
|
||||
}),
|
||||
},
|
||||
@@ -243,7 +351,7 @@ impl GethNode {
|
||||
self.shutdown()
|
||||
.context("Failed to gracefully shutdown after geth start error")?;
|
||||
return Err(err);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
@@ -255,7 +363,7 @@ impl GethNode {
|
||||
construct_concurrency_limited_provider::<Ethereum, _>(
|
||||
self.connection_string.as_str(),
|
||||
FallbackGasFiller::default(),
|
||||
ChainIdFiller::new(Some(CHAIN_ID)),
|
||||
ChainIdFiller::new(Some(self.chain_id)),
|
||||
NonceFiller::new(self.nonce_manager.clone()),
|
||||
self.wallet.clone(),
|
||||
)
|
||||
@@ -349,23 +457,25 @@ impl EthereumNode for GethNode {
|
||||
.context("Failed to submit transaction to geth node")?;
|
||||
let transaction_hash = *pending_transaction.tx_hash();
|
||||
|
||||
// The following is a fix for the "transaction indexing is in progress" error that we used
|
||||
// to get. You can find more information on this in the following GH issue in geth
|
||||
// https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on,
|
||||
// The following is a fix for the "transaction indexing is in progress" error that we
|
||||
// used to get. You can find more information on this in the following GH issue in
|
||||
// geth https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on,
|
||||
// before we can get the receipt of the transaction it needs to have been indexed by the
|
||||
// node's indexer. Just because the transaction has been confirmed it doesn't mean that it
|
||||
// has been indexed. When we call alloy's `get_receipt` it checks if the transaction was
|
||||
// confirmed. If it has been, then it will call `eth_getTransactionReceipt` method which
|
||||
// _might_ return the above error if the tx has not yet been indexed yet. So, we need to
|
||||
// implement a retry mechanism for the receipt to keep retrying to get it until it
|
||||
// eventually works, but we only do that if the error we get back is the "transaction
|
||||
// indexing is in progress" error or if the receipt is None.
|
||||
// node's indexer. Just because the transaction has been confirmed it doesn't mean that
|
||||
// it has been indexed. When we call alloy's `get_receipt` it checks if the
|
||||
// transaction was confirmed. If it has been, then it will call
|
||||
// `eth_getTransactionReceipt` method which _might_ return the above error if the tx
|
||||
// has not yet been indexed yet. So, we need to implement a retry mechanism for the
|
||||
// receipt to keep retrying to get it until it eventually works, but we only do that
|
||||
// if the error we get back is the "transaction indexing is in progress" error or if
|
||||
// the receipt is None.
|
||||
//
|
||||
// Getting the transaction indexed and taking a receipt can take a long time especially when
|
||||
// a lot of transactions are being submitted to the node. Thus, while initially we only
|
||||
// allowed for 60 seconds of waiting with a 1 second delay in polling, we need to allow for
|
||||
// a larger wait time. Therefore, in here we allow for 5 minutes of waiting with exponential
|
||||
// backoff each time we attempt to get the receipt and find that it's not available.
|
||||
// Getting the transaction indexed and taking a receipt can take a long time especially
|
||||
// when a lot of transactions are being submitted to the node. Thus, while initially
|
||||
// we only allowed for 60 seconds of waiting with a 1 second delay in polling, we
|
||||
// need to allow for a larger wait time. Therefore, in here we allow for 5 minutes of
|
||||
// waiting with exponential backoff each time we attempt to get the receipt and find
|
||||
// that it's not available.
|
||||
poll(
|
||||
Self::RECEIPT_POLLING_DURATION,
|
||||
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
||||
@@ -381,15 +491,12 @@ impl EthereumNode for GethNode {
|
||||
true => Ok(ControlFlow::Continue(())),
|
||||
false => Err(error.into()),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.instrument(tracing::info_span!(
|
||||
"Awaiting transaction receipt",
|
||||
?transaction_hash
|
||||
))
|
||||
.instrument(tracing::info_span!("Awaiting transaction receipt", ?transaction_hash))
|
||||
.await
|
||||
})
|
||||
}
|
||||
@@ -401,10 +508,8 @@ impl EthereumNode for GethNode {
|
||||
trace_options: GethDebugTracingOptions,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<GethTrace>> + '_>> {
|
||||
Box::pin(async move {
|
||||
let provider = self
|
||||
.provider()
|
||||
.await
|
||||
.context("Failed to create provider for tracing")?;
|
||||
let provider =
|
||||
self.provider().await.context("Failed to create provider for tracing")?;
|
||||
poll(
|
||||
Self::TRACE_POLLING_DURATION,
|
||||
PollingWaitBehavior::Constant(Duration::from_millis(200)),
|
||||
@@ -412,10 +517,7 @@ impl EthereumNode for GethNode {
|
||||
let provider = provider.clone();
|
||||
let trace_options = trace_options.clone();
|
||||
async move {
|
||||
match provider
|
||||
.debug_trace_transaction(tx_hash, trace_options)
|
||||
.await
|
||||
{
|
||||
match provider.debug_trace_transaction(tx_hash, trace_options).await {
|
||||
Ok(trace) => Ok(ControlFlow::Break(trace)),
|
||||
Err(error) => {
|
||||
let error_string = error.to_string();
|
||||
@@ -423,7 +525,7 @@ impl EthereumNode for GethNode {
|
||||
true => Ok(ControlFlow::Continue(())),
|
||||
false => Err(error.into()),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -542,6 +644,16 @@ impl EthereumNode for GethNode {
|
||||
as Pin<Box<dyn Stream<Item = MinedBlockInformation>>>)
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_signer_or_default(&self, address: Address) -> Address {
|
||||
let signer_addresses: Vec<_> =
|
||||
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet).collect();
|
||||
if signer_addresses.contains(&address) {
|
||||
address
|
||||
} else {
|
||||
self.wallet.default_signer().address()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GethNodeResolver {
|
||||
@@ -628,10 +740,7 @@ impl ResolverApi for GethNodeResolver {
|
||||
.context("Failed to get the geth block")?
|
||||
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
||||
.and_then(|block| {
|
||||
block
|
||||
.header
|
||||
.base_fee_per_gas
|
||||
.context("Failed to get the base fee per gas")
|
||||
block.header.base_fee_per_gas.context("Failed to get the base fee per gas")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -748,11 +857,7 @@ mod tests {
|
||||
// Arrange
|
||||
let (context, node) = shared_state();
|
||||
|
||||
let account_address = context
|
||||
.wallet_configuration
|
||||
.wallet()
|
||||
.default_signer()
|
||||
.address();
|
||||
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));
|
||||
@@ -775,10 +880,7 @@ mod tests {
|
||||
|
||||
// Assert
|
||||
let version = version.expect("Failed to get the version");
|
||||
assert!(
|
||||
version.starts_with("geth version"),
|
||||
"expected version string, got: '{version}'"
|
||||
);
|
||||
assert!(version.starts_with("geth version"), "expected version string, got: '{version}'");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -802,12 +904,8 @@ mod tests {
|
||||
let node = shared_node();
|
||||
|
||||
// Act
|
||||
let gas_limit = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_gas_limit(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let gas_limit =
|
||||
node.resolver().await.unwrap().block_gas_limit(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = gas_limit.expect("Failed to get the gas limit");
|
||||
@@ -820,12 +918,8 @@ mod tests {
|
||||
let node = shared_node();
|
||||
|
||||
// Act
|
||||
let coinbase = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_coinbase(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let coinbase =
|
||||
node.resolver().await.unwrap().block_coinbase(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = coinbase.expect("Failed to get the coinbase");
|
||||
@@ -838,12 +932,8 @@ mod tests {
|
||||
let node = shared_node();
|
||||
|
||||
// Act
|
||||
let block_difficulty = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_difficulty(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let block_difficulty =
|
||||
node.resolver().await.unwrap().block_difficulty(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = block_difficulty.expect("Failed to get the block difficulty");
|
||||
@@ -856,12 +946,7 @@ mod tests {
|
||||
let node = shared_node();
|
||||
|
||||
// Act
|
||||
let block_hash = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_hash(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let block_hash = node.resolver().await.unwrap().block_hash(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = block_hash.expect("Failed to get the block hash");
|
||||
@@ -874,12 +959,8 @@ mod tests {
|
||||
let node = shared_node();
|
||||
|
||||
// Act
|
||||
let block_timestamp = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_timestamp(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let block_timestamp =
|
||||
node.resolver().await.unwrap().block_timestamp(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = block_timestamp.expect("Failed to get the block timestamp");
|
||||
|
||||
@@ -116,7 +116,7 @@ impl LighthouseGethNode {
|
||||
const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress";
|
||||
const TRANSACTION_TRACING_ERROR: &str = "historical state not available in path scheme yet";
|
||||
|
||||
const RECEIPT_POLLING_DURATION: Duration = Duration::from_secs(5 * 60);
|
||||
const RECEIPT_POLLING_DURATION: Duration = Duration::from_secs(30);
|
||||
const TRACE_POLLING_DURATION: Duration = Duration::from_secs(60);
|
||||
|
||||
const VALIDATOR_MNEMONIC: &str = "giant issue aisle success illegal bike spike question tent bar rely arctic volcano long crawl hungry vocal artwork sniff fantasy very lucky have athlete";
|
||||
@@ -132,9 +132,7 @@ impl LighthouseGethNode {
|
||||
let wallet_configuration = AsRef::<WalletConfiguration>::as_ref(&context);
|
||||
let kurtosis_configuration = AsRef::<KurtosisConfiguration>::as_ref(&context);
|
||||
|
||||
let geth_directory = working_directory_configuration
|
||||
.as_path()
|
||||
.join(Self::BASE_DIRECTORY);
|
||||
let geth_directory = working_directory_configuration.as_path().join(Self::BASE_DIRECTORY);
|
||||
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
let base_directory = geth_directory.join(id.to_string());
|
||||
|
||||
@@ -147,10 +145,7 @@ impl LighthouseGethNode {
|
||||
http_connection_string: String::default(),
|
||||
enclave_name: format!(
|
||||
"enclave-{}-{}",
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Must not fail")
|
||||
.as_nanos(),
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).expect("Must not fail").as_nanos(),
|
||||
id
|
||||
),
|
||||
|
||||
@@ -526,15 +521,12 @@ impl LighthouseGethNode {
|
||||
true => Ok(ControlFlow::Continue(())),
|
||||
false => Err(error.into()),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.instrument(tracing::info_span!(
|
||||
"Awaiting transaction receipt",
|
||||
?transaction_hash
|
||||
))
|
||||
.instrument(tracing::info_span!("Awaiting transaction receipt", ?transaction_hash))
|
||||
.await
|
||||
})
|
||||
}
|
||||
@@ -623,9 +615,7 @@ impl EthereumNode for LighthouseGethNode {
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<GethTrace>> + '_>> {
|
||||
Box::pin(async move {
|
||||
let provider = Arc::new(
|
||||
self.http_provider()
|
||||
.await
|
||||
.context("Failed to create provider for tracing")?,
|
||||
self.http_provider().await.context("Failed to create provider for tracing")?,
|
||||
);
|
||||
poll(
|
||||
Self::TRACE_POLLING_DURATION,
|
||||
@@ -634,10 +624,7 @@ impl EthereumNode for LighthouseGethNode {
|
||||
let provider = provider.clone();
|
||||
let trace_options = trace_options.clone();
|
||||
async move {
|
||||
match provider
|
||||
.debug_trace_transaction(tx_hash, trace_options)
|
||||
.await
|
||||
{
|
||||
match provider.debug_trace_transaction(tx_hash, trace_options).await {
|
||||
Ok(trace) => Ok(ControlFlow::Break(trace)),
|
||||
Err(error) => {
|
||||
let error_string = error.to_string();
|
||||
@@ -645,7 +632,7 @@ impl EthereumNode for LighthouseGethNode {
|
||||
true => Ok(ControlFlow::Continue(())),
|
||||
false => Err(error.into()),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -761,6 +748,16 @@ impl EthereumNode for LighthouseGethNode {
|
||||
as Pin<Box<dyn Stream<Item = MinedBlockInformation>>>)
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_signer_or_default(&self, address: Address) -> Address {
|
||||
let signer_addresses: Vec<_> =
|
||||
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet).collect();
|
||||
if signer_addresses.contains(&address) {
|
||||
address
|
||||
} else {
|
||||
self.wallet.default_signer().address()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LighthouseGethNodeResolver<F: TxFiller<Ethereum>, P: Provider<Ethereum>> {
|
||||
@@ -849,10 +846,7 @@ impl<F: TxFiller<Ethereum>, P: Provider<Ethereum>> ResolverApi
|
||||
.context("Failed to get the geth block")?
|
||||
.context("Failed to get the Geth block, perhaps there are no blocks?")
|
||||
.and_then(|block| {
|
||||
block
|
||||
.header
|
||||
.base_fee_per_gas
|
||||
.context("Failed to get the base fee per gas")
|
||||
block.header.base_fee_per_gas.context("Failed to get the base fee per gas")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -906,11 +900,7 @@ impl Node for LighthouseGethNode {
|
||||
.spawn()
|
||||
.expect("Failed to spawn the enclave kill command");
|
||||
|
||||
if !child
|
||||
.wait()
|
||||
.expect("Failed to wait for the enclave kill command")
|
||||
.success()
|
||||
{
|
||||
if !child.wait().expect("Failed to wait for the enclave kill command").success() {
|
||||
let stdout = {
|
||||
let mut stdout = String::default();
|
||||
child
|
||||
@@ -1131,17 +1121,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "Ignored since they take a long time to run"]
|
||||
async fn node_mines_simple_transfer_transaction_and_returns_receipt() {
|
||||
// Arrange
|
||||
let (context, node) = new_node();
|
||||
node.fund_all_accounts().await.expect("Failed");
|
||||
|
||||
let account_address = context
|
||||
.wallet_configuration
|
||||
.wallet()
|
||||
.default_signer()
|
||||
.address();
|
||||
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));
|
||||
@@ -1164,10 +1149,7 @@ mod tests {
|
||||
|
||||
// Assert
|
||||
let version = version.expect("Failed to get the version");
|
||||
assert!(
|
||||
version.starts_with("CLI Version"),
|
||||
"expected version string, got: '{version}'"
|
||||
);
|
||||
assert!(version.starts_with("CLI Version"), "expected version string, got: '{version}'");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1191,12 +1173,8 @@ mod tests {
|
||||
let (_context, node) = new_node();
|
||||
|
||||
// Act
|
||||
let gas_limit = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_gas_limit(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let gas_limit =
|
||||
node.resolver().await.unwrap().block_gas_limit(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = gas_limit.expect("Failed to get the gas limit");
|
||||
@@ -1209,12 +1187,8 @@ mod tests {
|
||||
let (_context, node) = new_node();
|
||||
|
||||
// Act
|
||||
let coinbase = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_coinbase(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let coinbase =
|
||||
node.resolver().await.unwrap().block_coinbase(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = coinbase.expect("Failed to get the coinbase");
|
||||
@@ -1227,12 +1201,8 @@ mod tests {
|
||||
let (_context, node) = new_node();
|
||||
|
||||
// Act
|
||||
let block_difficulty = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_difficulty(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let block_difficulty =
|
||||
node.resolver().await.unwrap().block_difficulty(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = block_difficulty.expect("Failed to get the block difficulty");
|
||||
@@ -1245,12 +1215,7 @@ mod tests {
|
||||
let (_context, node) = new_node();
|
||||
|
||||
// Act
|
||||
let block_hash = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_hash(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let block_hash = node.resolver().await.unwrap().block_hash(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = block_hash.expect("Failed to get the block hash");
|
||||
@@ -1263,12 +1228,8 @@ mod tests {
|
||||
let (_context, node) = new_node();
|
||||
|
||||
// Act
|
||||
let block_timestamp = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_timestamp(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let block_timestamp =
|
||||
node.resolver().await.unwrap().block_timestamp(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = block_timestamp.expect("Failed to get the block timestamp");
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
mod common;
|
||||
|
||||
pub mod geth;
|
||||
pub mod lighthouse_geth;
|
||||
pub mod substrate;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,25 +3,17 @@
|
||||
//! ## Required Binaries
|
||||
//! This module requires the following binaries to be compiled and available in your PATH:
|
||||
//!
|
||||
//! 1. **polkadot-parachain**:
|
||||
//! ```bash
|
||||
//! git clone https://github.com/paritytech/polkadot-sdk.git
|
||||
//! cd polkadot-sdk
|
||||
//! cargo build --release --locked -p polkadot-parachain-bin --bin polkadot-parachain
|
||||
//! ```
|
||||
//! 1. **polkadot-parachain**: ```bash git clone https://github.com/paritytech/polkadot-sdk.git cd
|
||||
//! polkadot-sdk cargo build --release --locked -p polkadot-parachain-bin --bin
|
||||
//! polkadot-parachain ```
|
||||
//!
|
||||
//! 2. **eth-rpc** (Revive EVM RPC server):
|
||||
//! ```bash
|
||||
//! git clone https://github.com/paritytech/polkadot-sdk.git
|
||||
//! cd polkadot-sdk
|
||||
//! cargo build --locked --profile production -p pallet-revive-eth-rpc --bin eth-rpc
|
||||
//! ```
|
||||
//! 2. **eth-rpc** (Revive EVM RPC server): ```bash git clone https://github.com/paritytech/polkadot-sdk.git
|
||||
//! cd polkadot-sdk cargo build --locked --profile production -p pallet-revive-eth-rpc --bin
|
||||
//! eth-rpc ```
|
||||
//!
|
||||
//! 3. **polkadot** (for the relay chain):
|
||||
//! ```bash
|
||||
//! # In polkadot-sdk directory
|
||||
//! cargo build --locked --profile testnet --features fast-runtime --bin polkadot --bin polkadot-prepare-worker --bin polkadot-execute-worker
|
||||
//! ```
|
||||
//! 3. **polkadot** (for the relay chain): ```bash # In polkadot-sdk directory cargo build --locked
|
||||
//! --profile testnet --features fast-runtime --bin polkadot --bin polkadot-prepare-worker --bin
|
||||
//! polkadot-execute-worker ```
|
||||
//!
|
||||
//! Make sure to add the build output directories to your PATH or provide
|
||||
//! the full paths in your configuration.
|
||||
@@ -30,6 +22,7 @@ use std::{
|
||||
fs::{create_dir_all, remove_dir_all},
|
||||
path::PathBuf,
|
||||
pin::Pin,
|
||||
process::{Command, Stdio},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
@@ -39,8 +32,8 @@ use std::{
|
||||
|
||||
use alloy::{
|
||||
eips::BlockNumberOrTag,
|
||||
genesis::Genesis,
|
||||
network::EthereumWallet,
|
||||
genesis::{Genesis, GenesisAccount},
|
||||
network::{Ethereum, EthereumWallet, NetworkWallet},
|
||||
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
|
||||
providers::{
|
||||
Provider,
|
||||
@@ -54,13 +47,15 @@ use alloy::{
|
||||
};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use async_stream::stream;
|
||||
use futures::Stream;
|
||||
use futures::{Stream, StreamExt};
|
||||
use revive_common::EVMVersion;
|
||||
use revive_dt_common::fs::clear_directory;
|
||||
use revive_dt_config::*;
|
||||
use revive_dt_format::traits::ResolverApi;
|
||||
use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation};
|
||||
use serde_json::{Value as JsonValue, json};
|
||||
use sp_core::crypto::Ss58Codec;
|
||||
use sp_runtime::AccountId32;
|
||||
use tokio::sync::OnceCell;
|
||||
use tracing::instrument;
|
||||
use zombienet_sdk::{LocalFileSystem, NetworkConfigBuilder, NetworkConfigExt};
|
||||
@@ -68,25 +63,18 @@ use zombienet_sdk::{LocalFileSystem, NetworkConfigBuilder, NetworkConfigExt};
|
||||
use crate::{
|
||||
Node,
|
||||
constants::INITIAL_BALANCE,
|
||||
helpers::Process,
|
||||
node_implementations::common::{
|
||||
chainspec::export_and_patch_chainspec_json,
|
||||
process::{command_version, spawn_eth_rpc_process},
|
||||
revive::ReviveNetwork,
|
||||
},
|
||||
provider_utils::{
|
||||
ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider,
|
||||
execute_transaction,
|
||||
},
|
||||
helpers::{Process, ProcessReadinessWaitBehavior},
|
||||
node_implementations::substrate::ReviveNetwork,
|
||||
provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider},
|
||||
};
|
||||
|
||||
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
/// A Zombienet network where collator is `polkadot-parachain` node with `eth-rpc` [`ZombieNode`]
|
||||
/// abstracts away the details of managing the zombienet network and provides an interface to
|
||||
/// interact with the parachain's Ethereum RPC.
|
||||
/// A Zombienet network where collator is `polkadot-parachain` node with `eth-rpc`
|
||||
/// [`ZombieNode`] abstracts away the details of managing the zombienet network and provides
|
||||
/// an interface to interact with the parachain's Ethereum RPC.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ZombienetNode {
|
||||
pub struct ZombieNode {
|
||||
/* Node Identifier */
|
||||
id: u32,
|
||||
connection_string: String,
|
||||
@@ -114,7 +102,7 @@ pub struct ZombienetNode {
|
||||
provider: OnceCell<ConcreteProvider<ReviveNetwork, Arc<EthereumWallet>>>,
|
||||
}
|
||||
|
||||
impl ZombienetNode {
|
||||
impl ZombieNode {
|
||||
const BASE_DIRECTORY: &str = "zombienet";
|
||||
const DATA_DIRECTORY: &str = "data";
|
||||
const LOGS_DIRECTORY: &str = "logs";
|
||||
@@ -134,14 +122,10 @@ impl ZombienetNode {
|
||||
+ AsRef<EthRpcConfiguration>
|
||||
+ AsRef<WalletConfiguration>,
|
||||
) -> Self {
|
||||
let eth_proxy_binary = AsRef::<EthRpcConfiguration>::as_ref(&context)
|
||||
.path
|
||||
.to_owned();
|
||||
let eth_proxy_binary = AsRef::<EthRpcConfiguration>::as_ref(&context).path.to_owned();
|
||||
let working_directory_path = AsRef::<WorkingDirectoryConfiguration>::as_ref(&context);
|
||||
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
let base_directory = working_directory_path
|
||||
.join(Self::BASE_DIRECTORY)
|
||||
.join(id.to_string());
|
||||
let base_directory = working_directory_path.join(Self::BASE_DIRECTORY).join(id.to_string());
|
||||
let base_directory = base_directory.canonicalize().unwrap_or(base_directory);
|
||||
let logs_directory = base_directory.join(Self::LOGS_DIRECTORY);
|
||||
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
|
||||
@@ -163,7 +147,7 @@ impl ZombienetNode {
|
||||
}
|
||||
}
|
||||
|
||||
fn init(&mut self, mut genesis: Genesis) -> anyhow::Result<&mut Self> {
|
||||
fn init(&mut self, genesis: Genesis) -> anyhow::Result<&mut Self> {
|
||||
let _ = clear_directory(&self.base_directory);
|
||||
let _ = clear_directory(&self.logs_directory);
|
||||
|
||||
@@ -173,16 +157,7 @@ impl ZombienetNode {
|
||||
.context("Failed to create logs directory for zombie node")?;
|
||||
|
||||
let template_chainspec_path = self.base_directory.join(Self::CHAIN_SPEC_JSON_FILE);
|
||||
export_and_patch_chainspec_json(
|
||||
&self.polkadot_parachain_path,
|
||||
Self::EXPORT_CHAINSPEC_COMMAND,
|
||||
"asset-hub-westend-local",
|
||||
&template_chainspec_path,
|
||||
&mut genesis,
|
||||
&self.wallet,
|
||||
INITIAL_BALANCE,
|
||||
)?;
|
||||
|
||||
self.prepare_chainspec(template_chainspec_path.clone(), genesis)?;
|
||||
let polkadot_parachain_path = self
|
||||
.polkadot_parachain_path
|
||||
.to_str()
|
||||
@@ -191,35 +166,25 @@ impl ZombienetNode {
|
||||
let node_rpc_port = Self::NODE_BASE_RPC_PORT + self.id as u16;
|
||||
|
||||
let network_config = NetworkConfigBuilder::new()
|
||||
.with_relaychain(|relay_chain| {
|
||||
relay_chain
|
||||
.with_chain("westend-local")
|
||||
.with_relaychain(|r| {
|
||||
r.with_chain("westend-local")
|
||||
.with_default_command("polkadot")
|
||||
.with_node(|node| node.with_name("alice"))
|
||||
.with_node(|node| node.with_name("bob"))
|
||||
})
|
||||
.with_global_settings(|global_settings| {
|
||||
// global_settings.with_base_dir(&self.base_directory)
|
||||
global_settings
|
||||
})
|
||||
.with_parachain(|parachain| {
|
||||
parachain
|
||||
.with_id(Self::PARACHAIN_ID)
|
||||
.with_chain_spec_path(template_chainspec_path.to_path_buf())
|
||||
.with_global_settings(|g| g.with_base_dir(&self.base_directory))
|
||||
.with_parachain(|p| {
|
||||
p.with_id(Self::PARACHAIN_ID)
|
||||
.with_chain_spec_path(template_chainspec_path.to_str().unwrap())
|
||||
.with_chain("asset-hub-westend-local")
|
||||
.with_collator(|node_config| {
|
||||
node_config
|
||||
.with_name("Collator")
|
||||
.with_collator(|n| {
|
||||
n.with_name("Collator")
|
||||
.with_command(polkadot_parachain_path)
|
||||
.with_rpc_port(node_rpc_port)
|
||||
.with_args(vec![
|
||||
("--pool-limit", u32::MAX.to_string().as_str()).into(),
|
||||
("--pool-kbytes", u32::MAX.to_string().as_str()).into(),
|
||||
])
|
||||
})
|
||||
})
|
||||
.build()
|
||||
.map_err(|err| anyhow::anyhow!("Failed to build zombienet network config: {err:?}"))?;
|
||||
.map_err(|e| anyhow::anyhow!("Failed to build zombienet network config: {e:?}"))?;
|
||||
|
||||
self.node_rpc_port = Some(node_rpc_port);
|
||||
self.network_config = Some(network_config);
|
||||
@@ -228,14 +193,9 @@ impl ZombienetNode {
|
||||
}
|
||||
|
||||
fn spawn_process(&mut self) -> anyhow::Result<()> {
|
||||
let network_config = self
|
||||
.network_config
|
||||
.clone()
|
||||
.context("Node not initialized, call init() first")?;
|
||||
let network_config =
|
||||
self.network_config.clone().context("Node not initialized, call init() first")?;
|
||||
|
||||
// TODO: Look into the possibility of removing this in the future, perhaps by reintroducing
|
||||
// the blocking runtime abstraction and making it available to the entire program so that we
|
||||
// don't need to be spawning multiple different runtimes.
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let network = rt.block_on(async {
|
||||
network_config
|
||||
@@ -249,13 +209,30 @@ impl ZombienetNode {
|
||||
let node_url = format!("ws://localhost:{}", self.node_rpc_port.unwrap());
|
||||
let eth_rpc_port = Self::ETH_RPC_BASE_PORT + self.id as u16;
|
||||
|
||||
let eth_rpc_process = spawn_eth_rpc_process(
|
||||
let eth_rpc_process = Process::new(
|
||||
"proxy",
|
||||
self.logs_directory.as_path(),
|
||||
self.eth_proxy_binary.as_path(),
|
||||
&node_url,
|
||||
eth_rpc_port,
|
||||
&[], // extra args
|
||||
Self::ETH_RPC_READY_MARKER,
|
||||
|command, stdout_file, stderr_file| {
|
||||
command
|
||||
.arg("--node-rpc-url")
|
||||
.arg(node_url)
|
||||
.arg("--rpc-cors")
|
||||
.arg("all")
|
||||
.arg("--rpc-max-connections")
|
||||
.arg(u32::MAX.to_string())
|
||||
.arg("--rpc-port")
|
||||
.arg(eth_rpc_port.to_string())
|
||||
.stdout(stdout_file)
|
||||
.stderr(stderr_file);
|
||||
},
|
||||
ProcessReadinessWaitBehavior::TimeBoundedWaitFunction {
|
||||
max_wait_duration: Duration::from_secs(30),
|
||||
check_function: Box::new(|_, stderr_line| match stderr_line {
|
||||
Some(line) => Ok(line.contains(Self::ETH_RPC_READY_MARKER)),
|
||||
None => Ok(false),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
match eth_rpc_process {
|
||||
@@ -265,7 +242,7 @@ impl ZombienetNode {
|
||||
self.shutdown()
|
||||
.context("Failed to gracefully shutdown after eth proxy start error")?;
|
||||
return Err(err);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
tracing::debug!("eth-rpc is up");
|
||||
@@ -276,8 +253,109 @@ impl ZombienetNode {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare_chainspec(
|
||||
&mut self,
|
||||
template_chainspec_path: PathBuf,
|
||||
mut genesis: Genesis,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut cmd: Command = std::process::Command::new(&self.polkadot_parachain_path);
|
||||
cmd.arg(Self::EXPORT_CHAINSPEC_COMMAND)
|
||||
.arg("--chain")
|
||||
.arg("asset-hub-westend-local");
|
||||
|
||||
let output = cmd.output().context("Failed to export the chain-spec")?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Build chain-spec failed: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
let content = String::from_utf8(output.stdout)
|
||||
.context("Failed to decode collators chain-spec output as UTF-8")?;
|
||||
let mut chainspec_json: JsonValue =
|
||||
serde_json::from_str(&content).context("Failed to parse collators chain spec JSON")?;
|
||||
|
||||
let existing_chainspec_balances =
|
||||
chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"]
|
||||
.as_array()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut merged_balances: Vec<(String, u128)> = existing_chainspec_balances
|
||||
.into_iter()
|
||||
.filter_map(|val| {
|
||||
if let Some(arr) = val.as_array() {
|
||||
if arr.len() == 2 {
|
||||
let account = arr[0].as_str()?.to_string();
|
||||
let balance = arr[1].as_f64()? as u128;
|
||||
return Some((account, balance));
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut eth_balances = {
|
||||
for signer_address in
|
||||
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
|
||||
{
|
||||
// Note, the use of the entry API here means that we only modify the entries for any
|
||||
// account that is not in the `alloc` field of the genesis state.
|
||||
genesis
|
||||
.alloc
|
||||
.entry(signer_address)
|
||||
.or_insert(GenesisAccount::default().with_balance(U256::from(INITIAL_BALANCE)));
|
||||
}
|
||||
self.extract_balance_from_genesis_file(&genesis)
|
||||
.context("Failed to extract balances from EVM genesis JSON")?
|
||||
};
|
||||
|
||||
merged_balances.append(&mut eth_balances);
|
||||
|
||||
chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] =
|
||||
json!(merged_balances);
|
||||
|
||||
let writer = std::fs::File::create(&template_chainspec_path)
|
||||
.context("Failed to create template chainspec file")?;
|
||||
|
||||
serde_json::to_writer_pretty(writer, &chainspec_json)
|
||||
.context("Failed to write template chainspec JSON")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_balance_from_genesis_file(
|
||||
&self,
|
||||
genesis: &Genesis,
|
||||
) -> anyhow::Result<Vec<(String, u128)>> {
|
||||
genesis.alloc.iter().try_fold(Vec::new(), |mut vec, (address, acc)| {
|
||||
let polkadot_address = Self::eth_to_polkadot_address(address);
|
||||
let balance = acc.balance.try_into()?;
|
||||
vec.push((polkadot_address, balance));
|
||||
Ok(vec)
|
||||
})
|
||||
}
|
||||
|
||||
fn eth_to_polkadot_address(address: &Address) -> String {
|
||||
let eth_bytes = address.0.0;
|
||||
|
||||
let mut padded = [0xEEu8; 32];
|
||||
padded[..20].copy_from_slice(ð_bytes);
|
||||
|
||||
let account_id = AccountId32::from(padded);
|
||||
account_id.to_ss58check()
|
||||
}
|
||||
|
||||
pub fn eth_rpc_version(&self) -> anyhow::Result<String> {
|
||||
command_version(&self.eth_proxy_binary)
|
||||
let output = Command::new(&self.eth_proxy_binary)
|
||||
.arg("--version")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?
|
||||
.wait_with_output()?
|
||||
.stdout;
|
||||
|
||||
Ok(String::from_utf8_lossy(&output).trim().to_string())
|
||||
}
|
||||
|
||||
async fn provider(
|
||||
@@ -287,7 +365,7 @@ impl ZombienetNode {
|
||||
.get_or_try_init(|| async move {
|
||||
construct_concurrency_limited_provider::<ReviveNetwork, _>(
|
||||
self.connection_string.as_str(),
|
||||
FallbackGasFiller::new(u64::MAX, 5_000_000_000, 1_000_000_000),
|
||||
FallbackGasFiller::new(250_000_000, 5_000_000_000, 1_000_000_000),
|
||||
ChainIdFiller::default(), // TODO: use CHAIN_ID constant
|
||||
NonceFiller::new(self.nonce_manager.clone()),
|
||||
self.wallet.clone(),
|
||||
@@ -300,7 +378,7 @@ impl ZombienetNode {
|
||||
}
|
||||
}
|
||||
|
||||
impl EthereumNode for ZombienetNode {
|
||||
impl EthereumNode for ZombieNode {
|
||||
fn pre_transactions(&mut self) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + '_>> {
|
||||
Box::pin(async move { Ok(()) })
|
||||
}
|
||||
@@ -350,11 +428,21 @@ impl EthereumNode for ZombienetNode {
|
||||
transaction: alloy::rpc::types::TransactionRequest,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
|
||||
Box::pin(async move {
|
||||
let provider = self
|
||||
let pending = self
|
||||
.provider()
|
||||
.await
|
||||
.context("Failed to create the provider")?;
|
||||
execute_transaction(provider, transaction).await
|
||||
.context("Failed to create provider for transaction submission")?
|
||||
.send_transaction(transaction)
|
||||
.await
|
||||
.context("Failed to submit transaction to proxy")?;
|
||||
|
||||
let receipt =
|
||||
tokio::time::timeout(std::time::Duration::from_secs(120), pending.get_receipt())
|
||||
.await
|
||||
.context("Timeout waiting for transaction receipt")?
|
||||
.context("Failed to fetch transaction receipt from proxy")?;
|
||||
|
||||
Ok(receipt)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -448,21 +536,22 @@ impl EthereumNode for ZombienetNode {
|
||||
+ '_,
|
||||
>,
|
||||
> {
|
||||
fn create_stream(
|
||||
provider: ConcreteProvider<ReviveNetwork, Arc<EthereumWallet>>,
|
||||
) -> impl Stream<Item = MinedBlockInformation> {
|
||||
stream! {
|
||||
let mut block_number = provider.get_block_number().await.expect("Failed to get the block number");
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
Box::pin(async move {
|
||||
let provider = self
|
||||
.provider()
|
||||
.await
|
||||
.context("Failed to create the provider for block subscription")?;
|
||||
let mut block_subscription = provider
|
||||
.watch_full_blocks()
|
||||
.await
|
||||
.context("Failed to create the blocks stream")?;
|
||||
block_subscription.set_channel_size(0xFFFF);
|
||||
block_subscription.set_poll_interval(Duration::from_secs(1));
|
||||
let block_stream = block_subscription.into_stream();
|
||||
|
||||
let Ok(Some(block)) = provider.get_block_by_number(BlockNumberOrTag::Number(block_number)).await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
block_number += 1;
|
||||
yield MinedBlockInformation {
|
||||
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 _,
|
||||
@@ -473,22 +562,22 @@ impl EthereumNode for ZombienetNode {
|
||||
.as_hashes()
|
||||
.expect("Must be hashes")
|
||||
.to_vec(),
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Box::pin(async move {
|
||||
let provider = self
|
||||
.provider()
|
||||
.await
|
||||
.context("Failed to create the provider for a block subscription")?;
|
||||
|
||||
let stream = Box::pin(create_stream(provider))
|
||||
as Pin<Box<dyn Stream<Item = MinedBlockInformation>>>;
|
||||
|
||||
Ok(stream)
|
||||
})
|
||||
});
|
||||
|
||||
Ok(Box::pin(mined_block_information_stream)
|
||||
as Pin<Box<dyn Stream<Item = MinedBlockInformation>>>)
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_signer_or_default(&self, address: Address) -> Address {
|
||||
let signer_addresses: Vec<_> =
|
||||
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet).collect();
|
||||
if signer_addresses.contains(&address) {
|
||||
address
|
||||
} else {
|
||||
self.wallet.default_signer().address()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,10 +667,7 @@ impl<F: TxFiller<ReviveNetwork>, P: Provider<ReviveNetwork>> ResolverApi
|
||||
.context("Failed to get the zombie block")?
|
||||
.context("Failed to get the zombie block, perhaps the chain has no blocks?")
|
||||
.and_then(|block| {
|
||||
block
|
||||
.header
|
||||
.base_fee_per_gas
|
||||
.context("Failed to get the base fee per gas")
|
||||
block.header.base_fee_per_gas.context("Failed to get the base fee per gas")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -622,7 +708,7 @@ impl<F: TxFiller<ReviveNetwork>, P: Provider<ReviveNetwork>> ResolverApi
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for ZombienetNode {
|
||||
impl Node for ZombieNode {
|
||||
fn shutdown(&mut self) -> anyhow::Result<()> {
|
||||
// Kill the eth_rpc process
|
||||
drop(self.eth_rpc_process.take());
|
||||
@@ -653,11 +739,21 @@ impl Node for ZombienetNode {
|
||||
}
|
||||
|
||||
fn version(&self) -> anyhow::Result<String> {
|
||||
command_version(&self.polkadot_parachain_path)
|
||||
let output = Command::new(&self.polkadot_parachain_path)
|
||||
.arg("--version")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.context("Failed execute --version")?
|
||||
.wait_with_output()
|
||||
.context("Failed to wait --version")?
|
||||
.stdout;
|
||||
Ok(String::from_utf8_lossy(&output).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ZombienetNode {
|
||||
impl Drop for ZombieNode {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.shutdown();
|
||||
}
|
||||
@@ -667,9 +763,7 @@ impl Drop for ZombienetNode {
|
||||
mod tests {
|
||||
use alloy::rpc::types::TransactionRequest;
|
||||
|
||||
use crate::node_implementations::{
|
||||
common::chainspec::eth_to_polkadot_address, zombienet::tests::utils::shared_node,
|
||||
};
|
||||
use crate::node_implementations::zombienet::tests::utils::shared_node;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -683,12 +777,10 @@ mod tests {
|
||||
TestExecutionContext::default()
|
||||
}
|
||||
|
||||
pub async fn new_node() -> (TestExecutionContext, ZombienetNode) {
|
||||
pub async fn new_node() -> (TestExecutionContext, ZombieNode) {
|
||||
let context = test_config();
|
||||
let mut node = ZombienetNode::new(
|
||||
context.polkadot_parachain_configuration.path.clone(),
|
||||
&context,
|
||||
);
|
||||
let mut node =
|
||||
ZombieNode::new(context.polkadot_parachain_configuration.path.clone(), &context);
|
||||
let genesis = context.genesis_configuration.genesis().unwrap().clone();
|
||||
node.init(genesis).unwrap();
|
||||
|
||||
@@ -703,9 +795,8 @@ mod tests {
|
||||
(context, node)
|
||||
}
|
||||
|
||||
pub async fn shared_state() -> &'static (TestExecutionContext, Arc<ZombienetNode>) {
|
||||
static NODE: OnceCell<(TestExecutionContext, Arc<ZombienetNode>)> =
|
||||
OnceCell::const_new();
|
||||
pub async fn shared_state() -> &'static (TestExecutionContext, Arc<ZombieNode>) {
|
||||
static NODE: OnceCell<(TestExecutionContext, Arc<ZombieNode>)> = OnceCell::const_new();
|
||||
|
||||
NODE.get_or_init(|| async {
|
||||
let (context, node) = new_node().await;
|
||||
@@ -714,14 +805,13 @@ mod tests {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn shared_node() -> &'static Arc<ZombienetNode> {
|
||||
pub async fn shared_node() -> &'static Arc<ZombieNode> {
|
||||
&shared_state().await.1
|
||||
}
|
||||
}
|
||||
use utils::{new_node, test_config};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "Ignored for the time being"]
|
||||
async fn test_transfer_transaction_should_return_receipt() {
|
||||
let (ctx, node) = new_node().await;
|
||||
|
||||
@@ -755,29 +845,26 @@ mod tests {
|
||||
"#;
|
||||
|
||||
let context = test_config();
|
||||
let mut node = ZombienetNode::new(
|
||||
context.polkadot_parachain_configuration.path.clone(),
|
||||
&context,
|
||||
);
|
||||
let mut node =
|
||||
ZombieNode::new(context.polkadot_parachain_configuration.path.clone(), &context);
|
||||
|
||||
// Call `init()`
|
||||
node.init(serde_json::from_str(genesis_content).unwrap())
|
||||
.expect("init failed");
|
||||
node.init(serde_json::from_str(genesis_content).unwrap()).expect("init failed");
|
||||
|
||||
// Check that the patched chainspec file was generated
|
||||
let final_chainspec_path = node
|
||||
.base_directory
|
||||
.join(ZombienetNode::CHAIN_SPEC_JSON_FILE);
|
||||
let final_chainspec_path = node.base_directory.join(ZombieNode::CHAIN_SPEC_JSON_FILE);
|
||||
assert!(final_chainspec_path.exists(), "Chainspec file should exist");
|
||||
|
||||
let contents =
|
||||
std::fs::read_to_string(&final_chainspec_path).expect("Failed to read chainspec");
|
||||
|
||||
// Validate that the Polkadot addresses derived from the Ethereum addresses are in the file
|
||||
let first_eth_addr =
|
||||
eth_to_polkadot_address(&"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap());
|
||||
let second_eth_addr =
|
||||
eth_to_polkadot_address(&"Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2".parse().unwrap());
|
||||
let first_eth_addr = ZombieNode::eth_to_polkadot_address(
|
||||
&"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap(),
|
||||
);
|
||||
let second_eth_addr = ZombieNode::eth_to_polkadot_address(
|
||||
&"Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2".parse().unwrap(),
|
||||
);
|
||||
|
||||
assert!(
|
||||
contents.contains(&first_eth_addr),
|
||||
@@ -789,14 +876,91 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_parse_genesis_alloc() {
|
||||
// Create test genesis file
|
||||
let genesis_json = r#"
|
||||
{
|
||||
"alloc": {
|
||||
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1": { "balance": "1000000000000000000" },
|
||||
"0x0000000000000000000000000000000000000000": { "balance": "0xDE0B6B3A7640000" },
|
||||
"0xffffffffffffffffffffffffffffffffffffffff": { "balance": "123456789" }
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let context = test_config();
|
||||
let node = ZombieNode::new(context.polkadot_parachain_configuration.path.clone(), &context);
|
||||
|
||||
let result = node
|
||||
.extract_balance_from_genesis_file(&serde_json::from_str(genesis_json).unwrap())
|
||||
.unwrap();
|
||||
|
||||
let result_map: std::collections::HashMap<_, _> = result.into_iter().collect();
|
||||
|
||||
assert_eq!(
|
||||
result_map.get("5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV"),
|
||||
Some(&1_000_000_000_000_000_000u128)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
result_map.get("5C4hrfjw9DjXZTzV3MwzrrAr9P1MLDHajjSidz9bR544LEq1"),
|
||||
Some(&1_000_000_000_000_000_000u128)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
result_map.get("5HrN7fHLXWcFiXPwwtq2EkSGns9eMmoUQnbVKweNz3VVr6N4"),
|
||||
Some(&123_456_789u128)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_eth_to_polkadot_mappings() {
|
||||
let eth_addresses = vec![
|
||||
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
|
||||
"0xffffffffffffffffffffffffffffffffffffffff",
|
||||
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
|
||||
];
|
||||
|
||||
for eth_addr in eth_addresses {
|
||||
let ss58 = ZombieNode::eth_to_polkadot_address(ð_addr.parse().unwrap());
|
||||
|
||||
println!("Ethereum: {eth_addr} -> Polkadot SS58: {ss58}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eth_to_polkadot_address() {
|
||||
let cases = vec![
|
||||
(
|
||||
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
|
||||
"5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV",
|
||||
),
|
||||
(
|
||||
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
|
||||
"5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV",
|
||||
),
|
||||
(
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
"5C4hrfjw9DjXZTzV3MwzrrAr9P1MLDHajjSidz9bR544LEq1",
|
||||
),
|
||||
(
|
||||
"0xffffffffffffffffffffffffffffffffffffffff",
|
||||
"5HrN7fHLXWcFiXPwwtq2EkSGns9eMmoUQnbVKweNz3VVr6N4",
|
||||
),
|
||||
];
|
||||
|
||||
for (eth_addr, expected_ss58) in cases {
|
||||
let result = ZombieNode::eth_to_polkadot_address(ð_addr.parse().unwrap());
|
||||
assert_eq!(result, expected_ss58, "Mismatch for Ethereum address {eth_addr}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eth_rpc_version_works() {
|
||||
// Arrange
|
||||
let context = test_config();
|
||||
let node = ZombienetNode::new(
|
||||
context.polkadot_parachain_configuration.path.clone(),
|
||||
&context,
|
||||
);
|
||||
let node = ZombieNode::new(context.polkadot_parachain_configuration.path.clone(), &context);
|
||||
|
||||
// Act
|
||||
let version = node.eth_rpc_version().unwrap();
|
||||
@@ -812,10 +976,7 @@ mod tests {
|
||||
fn version_works() {
|
||||
// Arrange
|
||||
let context = test_config();
|
||||
let node = ZombienetNode::new(
|
||||
context.polkadot_parachain_configuration.path.clone(),
|
||||
&context,
|
||||
);
|
||||
let node = ZombieNode::new(context.polkadot_parachain_configuration.path.clone(), &context);
|
||||
|
||||
// Act
|
||||
let version = node.version().unwrap();
|
||||
@@ -853,12 +1014,8 @@ mod tests {
|
||||
let node = shared_node().await;
|
||||
|
||||
// Act
|
||||
let gas_limit = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_gas_limit(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let gas_limit =
|
||||
node.resolver().await.unwrap().block_gas_limit(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = gas_limit.expect("Failed to get the gas limit");
|
||||
@@ -871,12 +1028,8 @@ mod tests {
|
||||
let node = shared_node().await;
|
||||
|
||||
// Act
|
||||
let coinbase = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_coinbase(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let coinbase =
|
||||
node.resolver().await.unwrap().block_coinbase(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = coinbase.expect("Failed to get the coinbase");
|
||||
@@ -889,12 +1042,8 @@ mod tests {
|
||||
let node = shared_node().await;
|
||||
|
||||
// Act
|
||||
let block_difficulty = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_difficulty(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let block_difficulty =
|
||||
node.resolver().await.unwrap().block_difficulty(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = block_difficulty.expect("Failed to get the block difficulty");
|
||||
@@ -907,12 +1056,7 @@ mod tests {
|
||||
let node = shared_node().await;
|
||||
|
||||
// Act
|
||||
let block_hash = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_hash(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let block_hash = node.resolver().await.unwrap().block_hash(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = block_hash.expect("Failed to get the block hash");
|
||||
@@ -925,12 +1069,8 @@ mod tests {
|
||||
let node = shared_node().await;
|
||||
|
||||
// Act
|
||||
let block_timestamp = node
|
||||
.resolver()
|
||||
.await
|
||||
.unwrap()
|
||||
.block_timestamp(BlockNumberOrTag::Latest)
|
||||
.await;
|
||||
let block_timestamp =
|
||||
node.resolver().await.unwrap().block_timestamp(BlockNumberOrTag::Latest).await;
|
||||
|
||||
// Assert
|
||||
let _ = block_timestamp.expect("Failed to get the block timestamp");
|
||||
|
||||
@@ -11,9 +11,7 @@ pub struct ConcurrencyLimiterLayer {
|
||||
|
||||
impl ConcurrencyLimiterLayer {
|
||||
pub fn new(permit_count: usize) -> Self {
|
||||
Self {
|
||||
semaphore: Arc::new(Semaphore::new(permit_count)),
|
||||
}
|
||||
Self { semaphore: Arc::new(Semaphore::new(permit_count)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +19,7 @@ impl<S> Layer<S> for ConcurrencyLimiterLayer {
|
||||
type Service = ConcurrencyLimiterService<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
ConcurrencyLimiterService {
|
||||
service: inner,
|
||||
semaphore: self.semaphore.clone(),
|
||||
}
|
||||
ConcurrencyLimiterService { service: inner, semaphore: self.semaphore.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +50,7 @@ where
|
||||
let future = self.service.call(req);
|
||||
|
||||
Box::pin(async move {
|
||||
let _permit = semaphore
|
||||
.acquire()
|
||||
.await
|
||||
.expect("Semaphore has been closed");
|
||||
let _permit = semaphore.acquire().await.expect("Semaphore has been closed");
|
||||
tracing::debug!(
|
||||
available_permits = semaphore.available_permits(),
|
||||
"Acquired Semaphore Permit"
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
use alloy::{
|
||||
network::{Network, TransactionBuilder},
|
||||
providers::{
|
||||
Provider, SendableTx,
|
||||
fillers::{GasFiller, TxFiller},
|
||||
},
|
||||
transports::TransportResult,
|
||||
};
|
||||
|
||||
// Percentage padding applied to estimated gas (e.g. 120 = 20% padding)
|
||||
const GAS_ESTIMATE_PADDING_NUMERATOR: u64 = 120;
|
||||
const GAS_ESTIMATE_PADDING_DENOMINATOR: u64 = 100;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FallbackGasFiller {
|
||||
inner: GasFiller,
|
||||
default_gas_limit: u64,
|
||||
default_max_fee_per_gas: u128,
|
||||
default_priority_fee: u128,
|
||||
}
|
||||
|
||||
impl FallbackGasFiller {
|
||||
pub fn new(
|
||||
default_gas_limit: u64,
|
||||
default_max_fee_per_gas: u128,
|
||||
default_priority_fee: u128,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: GasFiller,
|
||||
default_gas_limit,
|
||||
default_max_fee_per_gas,
|
||||
default_priority_fee,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FallbackGasFiller {
|
||||
fn default() -> Self {
|
||||
FallbackGasFiller::new(25_000_000, 1_000_000_000, 1_000_000_000)
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> TxFiller<N> for FallbackGasFiller
|
||||
where
|
||||
N: Network,
|
||||
{
|
||||
type Fillable = Option<<GasFiller as TxFiller<N>>::Fillable>;
|
||||
|
||||
fn status(
|
||||
&self,
|
||||
tx: &<N as Network>::TransactionRequest,
|
||||
) -> alloy::providers::fillers::FillerControlFlow {
|
||||
<GasFiller as TxFiller<N>>::status(&self.inner, tx)
|
||||
}
|
||||
|
||||
fn fill_sync(&self, _: &mut alloy::providers::SendableTx<N>) {}
|
||||
|
||||
async fn prepare<P: Provider<N>>(
|
||||
&self,
|
||||
provider: &P,
|
||||
tx: &<N as Network>::TransactionRequest,
|
||||
) -> TransportResult<Self::Fillable> {
|
||||
match self.inner.prepare(provider, tx).await {
|
||||
Ok(fill) => Ok(Some(fill)),
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn fill(
|
||||
&self,
|
||||
fillable: Self::Fillable,
|
||||
mut tx: alloy::providers::SendableTx<N>,
|
||||
) -> TransportResult<SendableTx<N>> {
|
||||
if let Some(fill) = fillable {
|
||||
let mut tx = self.inner.fill(fill, tx).await?;
|
||||
if let Some(builder) = tx.as_mut_builder() {
|
||||
if let Some(estimated) = builder.gas_limit() {
|
||||
let padded = estimated
|
||||
.checked_mul(GAS_ESTIMATE_PADDING_NUMERATOR)
|
||||
.and_then(|v| v.checked_div(GAS_ESTIMATE_PADDING_DENOMINATOR))
|
||||
.unwrap_or(u64::MAX);
|
||||
builder.set_gas_limit(padded);
|
||||
}
|
||||
}
|
||||
Ok(tx)
|
||||
} else {
|
||||
if let Some(builder) = tx.as_mut_builder() {
|
||||
builder.set_gas_limit(self.default_gas_limit);
|
||||
builder.set_max_fee_per_gas(self.default_max_fee_per_gas);
|
||||
builder.set_max_priority_fee_per_gas(self.default_priority_fee);
|
||||
}
|
||||
Ok(tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use alloy::{
|
||||
network::{Network, TransactionBuilder},
|
||||
providers::{
|
||||
Provider, SendableTx,
|
||||
fillers::{GasFiller, TxFiller},
|
||||
},
|
||||
transports::TransportResult,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FallbackGasFiller {
|
||||
inner: GasFiller,
|
||||
default_gas_limit: u64,
|
||||
default_max_fee_per_gas: u128,
|
||||
default_priority_fee: u128,
|
||||
}
|
||||
|
||||
impl FallbackGasFiller {
|
||||
pub fn new(
|
||||
default_gas_limit: u64,
|
||||
default_max_fee_per_gas: u128,
|
||||
default_priority_fee: u128,
|
||||
) -> Self {
|
||||
Self { inner: GasFiller, default_gas_limit, default_max_fee_per_gas, default_priority_fee }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FallbackGasFiller {
|
||||
fn default() -> Self {
|
||||
FallbackGasFiller::new(10_000_000, 1_000_000_000, 1_000_000_000)
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> TxFiller<N> for FallbackGasFiller
|
||||
where
|
||||
N: Network,
|
||||
{
|
||||
type Fillable = Option<<GasFiller as TxFiller<N>>::Fillable>;
|
||||
|
||||
fn status(
|
||||
&self,
|
||||
tx: &<N as Network>::TransactionRequest,
|
||||
) -> alloy::providers::fillers::FillerControlFlow {
|
||||
<GasFiller as TxFiller<N>>::status(&self.inner, tx)
|
||||
}
|
||||
|
||||
fn fill_sync(&self, _: &mut alloy::providers::SendableTx<N>) {}
|
||||
|
||||
async fn prepare<P: Provider<N>>(
|
||||
&self,
|
||||
provider: &P,
|
||||
tx: &<N as Network>::TransactionRequest,
|
||||
) -> TransportResult<Self::Fillable> {
|
||||
// Try to fetch GasFiller's "fillable" (gas_price, base_fee, estimate_gas, …)
|
||||
// Propagate errors so caller can handle them appropriately
|
||||
self.inner.prepare(provider, tx).await.map(Some)
|
||||
}
|
||||
|
||||
async fn fill(
|
||||
&self,
|
||||
fillable: Self::Fillable,
|
||||
mut tx: alloy::providers::SendableTx<N>,
|
||||
) -> TransportResult<SendableTx<N>> {
|
||||
if let Some(fill) = fillable {
|
||||
// our inner GasFiller succeeded — use it
|
||||
self.inner.fill(fill, tx).await
|
||||
} else {
|
||||
if let Some(builder) = tx.as_mut_builder() {
|
||||
builder.set_gas_limit(self.default_gas_limit);
|
||||
builder.set_max_fee_per_gas(self.default_max_fee_per_gas);
|
||||
builder.set_max_priority_fee_per_gas(self.default_priority_fee);
|
||||
}
|
||||
Ok(tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
mod concurrency_limiter;
|
||||
mod fallback_gas_filler;
|
||||
mod fallback_gas_provider;
|
||||
mod provider;
|
||||
|
||||
pub use concurrency_limiter::*;
|
||||
pub use fallback_gas_filler::*;
|
||||
pub use fallback_gas_provider::*;
|
||||
pub use provider::*;
|
||||
|
||||
@@ -10,7 +10,7 @@ use alloy::{
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use revive_dt_common::futures::{PollingWaitBehavior, poll};
|
||||
use tracing::{Instrument, debug, info, info_span};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::provider_utils::{ConcurrencyLimiterLayer, FallbackGasFiller};
|
||||
|
||||
@@ -44,7 +44,7 @@ where
|
||||
// requests at any point of time and no more than that. This is done in an effort to stabilize
|
||||
// the framework from some of the interment issues that we've been seeing related to RPC calls.
|
||||
static GLOBAL_CONCURRENCY_LIMITER_LAYER: LazyLock<ConcurrencyLimiterLayer> =
|
||||
LazyLock::new(|| ConcurrencyLimiterLayer::new(500));
|
||||
LazyLock::new(|| ConcurrencyLimiterLayer::new(10));
|
||||
|
||||
let client = ClientBuilder::default()
|
||||
.layer(GLOBAL_CONCURRENCY_LIMITER_LAYER.clone())
|
||||
@@ -80,10 +80,8 @@ where
|
||||
NonceFiller: TxFiller<N>,
|
||||
WalletFiller<W>: TxFiller<N>,
|
||||
{
|
||||
let sendable_transaction = provider
|
||||
.fill(transaction)
|
||||
.await
|
||||
.context("Failed to fill transaction")?;
|
||||
let sendable_transaction =
|
||||
provider.fill(transaction).await.context("Failed to fill transaction")?;
|
||||
|
||||
let transaction_envelope = sendable_transaction
|
||||
.try_into_envelope()
|
||||
@@ -100,33 +98,27 @@ where
|
||||
} 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}"
|
||||
))?;
|
||||
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)),
|
||||
|| {
|
||||
debug!(%tx_hash, "Transaction included, polling for receipt");
|
||||
|
||||
poll(Duration::from_secs(30), 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(Some(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}"))
|
||||
.context(format!("Polling for receipt timed out for {tx_hash}"))
|
||||
}
|
||||
|
||||
@@ -66,50 +66,45 @@ impl ReportAggregator {
|
||||
match event {
|
||||
RunnerEvent::SubscribeToEvents(event) => {
|
||||
self.handle_subscribe_to_events_event(*event);
|
||||
}
|
||||
RunnerEvent::CorpusFileDiscovery(event) => {
|
||||
self.handle_corpus_file_discovered_event(*event)
|
||||
}
|
||||
},
|
||||
RunnerEvent::CorpusFileDiscovery(event) =>
|
||||
self.handle_corpus_file_discovered_event(*event),
|
||||
RunnerEvent::MetadataFileDiscovery(event) => {
|
||||
self.handle_metadata_file_discovery_event(*event);
|
||||
}
|
||||
},
|
||||
RunnerEvent::TestCaseDiscovery(event) => {
|
||||
self.handle_test_case_discovery(*event);
|
||||
}
|
||||
},
|
||||
RunnerEvent::TestSucceeded(event) => {
|
||||
self.handle_test_succeeded_event(*event);
|
||||
}
|
||||
},
|
||||
RunnerEvent::TestFailed(event) => {
|
||||
self.handle_test_failed_event(*event);
|
||||
}
|
||||
},
|
||||
RunnerEvent::TestIgnored(event) => {
|
||||
self.handle_test_ignored_event(*event);
|
||||
}
|
||||
},
|
||||
RunnerEvent::NodeAssigned(event) => {
|
||||
self.handle_node_assigned_event(*event);
|
||||
}
|
||||
RunnerEvent::PreLinkContractsCompilationSucceeded(event) => {
|
||||
self.handle_pre_link_contracts_compilation_succeeded_event(*event)
|
||||
}
|
||||
RunnerEvent::PostLinkContractsCompilationSucceeded(event) => {
|
||||
self.handle_post_link_contracts_compilation_succeeded_event(*event)
|
||||
}
|
||||
RunnerEvent::PreLinkContractsCompilationFailed(event) => {
|
||||
self.handle_pre_link_contracts_compilation_failed_event(*event)
|
||||
}
|
||||
RunnerEvent::PostLinkContractsCompilationFailed(event) => {
|
||||
self.handle_post_link_contracts_compilation_failed_event(*event)
|
||||
}
|
||||
},
|
||||
RunnerEvent::PreLinkContractsCompilationSucceeded(event) =>
|
||||
self.handle_pre_link_contracts_compilation_succeeded_event(*event),
|
||||
RunnerEvent::PostLinkContractsCompilationSucceeded(event) =>
|
||||
self.handle_post_link_contracts_compilation_succeeded_event(*event),
|
||||
RunnerEvent::PreLinkContractsCompilationFailed(event) =>
|
||||
self.handle_pre_link_contracts_compilation_failed_event(*event),
|
||||
RunnerEvent::PostLinkContractsCompilationFailed(event) =>
|
||||
self.handle_post_link_contracts_compilation_failed_event(*event),
|
||||
RunnerEvent::LibrariesDeployed(event) => {
|
||||
self.handle_libraries_deployed_event(*event);
|
||||
}
|
||||
},
|
||||
RunnerEvent::ContractDeployed(event) => {
|
||||
self.handle_contract_deployed_event(*event);
|
||||
}
|
||||
},
|
||||
RunnerEvent::Completion(event) => {
|
||||
self.handle_completion(*event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
debug!("Report aggregation completed");
|
||||
@@ -123,12 +118,8 @@ impl ReportAggregator {
|
||||
file_name.push_str(".json");
|
||||
file_name
|
||||
};
|
||||
let file_path = self
|
||||
.report
|
||||
.context
|
||||
.working_directory_configuration()
|
||||
.as_path()
|
||||
.join(file_name);
|
||||
let file_path =
|
||||
self.report.context.working_directory_configuration().as_path().join(file_name);
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
@@ -136,10 +127,7 @@ impl ReportAggregator {
|
||||
.read(false)
|
||||
.open(&file_path)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to open report file for writing: {}",
|
||||
file_path.display()
|
||||
)
|
||||
format!("Failed to open report file for writing: {}", file_path.display())
|
||||
})?;
|
||||
serde_json::to_writer_pretty(&file, &self.report).with_context(|| {
|
||||
format!("Failed to serialize report JSON to {}", file_path.display())
|
||||
@@ -180,9 +168,8 @@ impl ReportAggregator {
|
||||
|
||||
// Add information on the fact that the case was ignored to the report.
|
||||
let test_case_report = self.test_case_report(&event.test_specifier);
|
||||
test_case_report.status = Some(TestCaseStatus::Succeeded {
|
||||
steps_executed: event.steps_executed,
|
||||
});
|
||||
test_case_report.status =
|
||||
Some(TestCaseStatus::Succeeded { steps_executed: event.steps_executed });
|
||||
self.handle_post_test_case_status_update(&event.test_specifier);
|
||||
}
|
||||
|
||||
@@ -197,9 +184,7 @@ impl ReportAggregator {
|
||||
|
||||
// Add information on the fact that the case was ignored to the report.
|
||||
let test_case_report = self.test_case_report(&event.test_specifier);
|
||||
test_case_report.status = Some(TestCaseStatus::Failed {
|
||||
reason: event.reason,
|
||||
});
|
||||
test_case_report.status = Some(TestCaseStatus::Failed { reason: event.reason });
|
||||
self.handle_post_test_case_status_update(&event.test_specifier);
|
||||
}
|
||||
|
||||
@@ -241,10 +226,7 @@ impl ReportAggregator {
|
||||
.or_default()
|
||||
.iter()
|
||||
.map(|(case_idx, case_report)| {
|
||||
(
|
||||
*case_idx,
|
||||
case_report.status.clone().expect("Can't be uninitialized"),
|
||||
)
|
||||
(*case_idx, case_report.status.clone().expect("Can't be uninitialized"))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let event = ReporterEvent::MetadataFileSolcModeCombinationExecutionCompleted {
|
||||
@@ -276,29 +258,13 @@ impl ReportAggregator {
|
||||
&mut self,
|
||||
event: PreLinkContractsCompilationSucceededEvent,
|
||||
) {
|
||||
let include_input = self
|
||||
.report
|
||||
.context
|
||||
.report_configuration()
|
||||
.include_compiler_input;
|
||||
let include_output = self
|
||||
.report
|
||||
.context
|
||||
.report_configuration()
|
||||
.include_compiler_output;
|
||||
let include_input = self.report.context.report_configuration().include_compiler_input;
|
||||
let include_output = self.report.context.report_configuration().include_compiler_output;
|
||||
|
||||
let execution_information = self.execution_information(&event.execution_specifier);
|
||||
|
||||
let compiler_input = if include_input {
|
||||
event.compiler_input
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let compiler_output = if include_output {
|
||||
Some(event.compiler_output)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let compiler_input = if include_input { event.compiler_input } else { None };
|
||||
let compiler_output = if include_output { Some(event.compiler_output) } else { None };
|
||||
|
||||
execution_information.pre_link_compilation_status = Some(CompilationStatus::Success {
|
||||
is_cached: event.is_cached,
|
||||
@@ -313,29 +279,13 @@ impl ReportAggregator {
|
||||
&mut self,
|
||||
event: PostLinkContractsCompilationSucceededEvent,
|
||||
) {
|
||||
let include_input = self
|
||||
.report
|
||||
.context
|
||||
.report_configuration()
|
||||
.include_compiler_input;
|
||||
let include_output = self
|
||||
.report
|
||||
.context
|
||||
.report_configuration()
|
||||
.include_compiler_output;
|
||||
let include_input = self.report.context.report_configuration().include_compiler_input;
|
||||
let include_output = self.report.context.report_configuration().include_compiler_output;
|
||||
|
||||
let execution_information = self.execution_information(&event.execution_specifier);
|
||||
|
||||
let compiler_input = if include_input {
|
||||
event.compiler_input
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let compiler_output = if include_output {
|
||||
Some(event.compiler_output)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let compiler_input = if include_input { event.compiler_input } else { None };
|
||||
let compiler_output = if include_output { Some(event.compiler_output) } else { None };
|
||||
|
||||
execution_information.post_link_compilation_status = Some(CompilationStatus::Success {
|
||||
is_cached: event.is_cached,
|
||||
@@ -375,8 +325,8 @@ impl ReportAggregator {
|
||||
}
|
||||
|
||||
fn handle_libraries_deployed_event(&mut self, event: LibrariesDeployedEvent) {
|
||||
self.execution_information(&event.execution_specifier)
|
||||
.deployed_libraries = Some(event.libraries);
|
||||
self.execution_information(&event.execution_specifier).deployed_libraries =
|
||||
Some(event.libraries);
|
||||
}
|
||||
|
||||
fn handle_contract_deployed_event(&mut self, event: ContractDeployedEvent) {
|
||||
@@ -442,7 +392,8 @@ impl Report {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Default)]
|
||||
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")]
|
||||
pub status: Option<TestCaseStatus>,
|
||||
/// Information related to the execution on one of the platforms.
|
||||
|
||||
@@ -8,8 +8,10 @@ use anyhow::Context as _;
|
||||
use indexmap::IndexMap;
|
||||
use revive_dt_common::types::PlatformIdentifier;
|
||||
use revive_dt_compiler::{CompilerInput, CompilerOutput};
|
||||
use revive_dt_format::metadata::Metadata;
|
||||
use revive_dt_format::{corpus::Corpus, metadata::ContractInstance};
|
||||
use revive_dt_format::{
|
||||
corpus::Corpus,
|
||||
metadata::{ContractInstance, Metadata},
|
||||
};
|
||||
use semver::Version;
|
||||
use tokio::sync::{broadcast, oneshot};
|
||||
|
||||
|
||||
@@ -34,19 +34,11 @@ pub(crate) async fn get_or_download(
|
||||
}
|
||||
|
||||
create_dir_all(&target_directory).with_context(|| {
|
||||
format!(
|
||||
"Failed to create solc cache directory: {}",
|
||||
target_directory.display()
|
||||
)
|
||||
format!("Failed to create solc cache directory: {}", target_directory.display())
|
||||
})?;
|
||||
download_to_file(&target_file, downloader)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to write downloaded solc to {}",
|
||||
target_file.display()
|
||||
)
|
||||
})?;
|
||||
.with_context(|| format!("Failed to write downloaded solc to {}", target_file.display()))?;
|
||||
cache.insert(target_file.clone());
|
||||
|
||||
Ok((downloader.version.clone(), target_file))
|
||||
@@ -70,12 +62,7 @@ async fn download_to_file(path: &Path, downloader: &SolcDownloader) -> anyhow::R
|
||||
}
|
||||
|
||||
let mut file = BufWriter::new(file);
|
||||
file.write_all(
|
||||
&downloader
|
||||
.download()
|
||||
.await
|
||||
.context("Failed to download solc binary bytes")?,
|
||||
)
|
||||
file.write_all(&downloader.download().await.context("Failed to download solc binary bytes")?)
|
||||
.with_context(|| format!("Failed to write solc binary to {}", path.display()))?;
|
||||
file.flush()
|
||||
.with_context(|| format!("Failed to flush file {}", path.display()))?;
|
||||
@@ -91,17 +78,11 @@ async fn download_to_file(path: &Path, downloader: &SolcDownloader) -> anyhow::R
|
||||
.stdout(std::process::Stdio::null())
|
||||
.spawn()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to spawn xattr to remove quarantine attribute on {}",
|
||||
path.display()
|
||||
)
|
||||
format!("Failed to spawn xattr to remove quarantine attribute on {}", path.display())
|
||||
})?
|
||||
.wait()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed waiting for xattr operation to complete on {}",
|
||||
path.display()
|
||||
)
|
||||
format!("Failed waiting for xattr operation to complete on {}", path.display())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -67,11 +67,7 @@ impl SolcDownloader {
|
||||
) -> anyhow::Result<Self> {
|
||||
let version_or_requirement = version.into();
|
||||
match version_or_requirement {
|
||||
VersionOrRequirement::Version(version) => Ok(Self {
|
||||
version,
|
||||
target,
|
||||
list,
|
||||
}),
|
||||
VersionOrRequirement::Version(version) => Ok(Self { version, target, list }),
|
||||
VersionOrRequirement::Requirement(requirement) => {
|
||||
let Some(version) = List::download(list)
|
||||
.await
|
||||
@@ -84,12 +80,8 @@ impl SolcDownloader {
|
||||
else {
|
||||
anyhow::bail!("Failed to find a version that satisfies {requirement:?}");
|
||||
};
|
||||
Ok(Self {
|
||||
version,
|
||||
target,
|
||||
list,
|
||||
})
|
||||
}
|
||||
Ok(Self { version, target, list })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,11 +122,7 @@ impl SolcDownloader {
|
||||
})?;
|
||||
|
||||
let path = build.path.clone();
|
||||
let expected_digest = build
|
||||
.sha256
|
||||
.strip_prefix("0x")
|
||||
.unwrap_or(&build.sha256)
|
||||
.to_string();
|
||||
let expected_digest = build.sha256.strip_prefix("0x").unwrap_or(&build.sha256).to_string();
|
||||
let url = format!("{}/{}/{}", Self::BASE_URL, self.target, path.display());
|
||||
|
||||
let file = reqwest::get(&url)
|
||||
@@ -159,54 +147,25 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn try_get_windows() {
|
||||
let version = List::download(List::WINDOWS_URL)
|
||||
.await
|
||||
.unwrap()
|
||||
.latest_release;
|
||||
SolcDownloader::windows(version)
|
||||
.await
|
||||
.unwrap()
|
||||
.download()
|
||||
.await
|
||||
.unwrap();
|
||||
let version = List::download(List::WINDOWS_URL).await.unwrap().latest_release;
|
||||
SolcDownloader::windows(version).await.unwrap().download().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn try_get_macosx() {
|
||||
let version = List::download(List::MACOSX_URL)
|
||||
.await
|
||||
.unwrap()
|
||||
.latest_release;
|
||||
SolcDownloader::macosx(version)
|
||||
.await
|
||||
.unwrap()
|
||||
.download()
|
||||
.await
|
||||
.unwrap();
|
||||
let version = List::download(List::MACOSX_URL).await.unwrap().latest_release;
|
||||
SolcDownloader::macosx(version).await.unwrap().download().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn try_get_linux() {
|
||||
let version = List::download(List::LINUX_URL)
|
||||
.await
|
||||
.unwrap()
|
||||
.latest_release;
|
||||
SolcDownloader::linux(version)
|
||||
.await
|
||||
.unwrap()
|
||||
.download()
|
||||
.await
|
||||
.unwrap();
|
||||
let version = List::download(List::LINUX_URL).await.unwrap().latest_release;
|
||||
SolcDownloader::linux(version).await.unwrap().download().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn try_get_wasm() {
|
||||
let version = List::download(List::WASM_URL).await.unwrap().latest_release;
|
||||
SolcDownloader::wasm(version)
|
||||
.await
|
||||
.unwrap()
|
||||
.download()
|
||||
.await
|
||||
.unwrap();
|
||||
SolcDownloader::wasm(version).await.unwrap().download().await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -93,16 +93,17 @@ echo ""
|
||||
# Run the tool
|
||||
cargo build --release;
|
||||
RUST_LOG="info,alloy_pubsub::service=error" ./target/release/retester test \
|
||||
--platform geth-evm-solc \
|
||||
--platform revive-dev-node-revm-solc \
|
||||
--corpus "$CORPUS_FILE" \
|
||||
--working-directory "$WORKDIR" \
|
||||
--concurrency.number-of-nodes 10 \
|
||||
--concurrency.number-of-threads 5 \
|
||||
--concurrency.ignore-concurrency-limit \
|
||||
--concurrency.number-of-concurrent-tasks 1000 \
|
||||
--wallet.additional-keys 100000 \
|
||||
--kitchensink.path "$SUBSTRATE_NODE_BIN" \
|
||||
--revive-dev-node.path "$REVIVE_DEV_NODE_BIN" \
|
||||
--eth-rpc.path "$ETH_RPC_BIN" \
|
||||
> logs.log
|
||||
> logs.log \
|
||||
2> output.log
|
||||
|
||||
echo -e "${GREEN}=== Test run completed! ===${NC}"
|
||||
Reference in New Issue
Block a user