Compare commits

..

8 Commits

Author SHA1 Message Date
Omar Abdulla 2a9d497827 Update the initial balance 2025-07-24 11:40:33 +03:00
Omar Abdulla 4763d763f6 Revert "Add extra comments"
This reverts commit bd4de2c83d.
2025-07-24 10:38:23 +03:00
Omar Abdulla bd4de2c83d Add extra comments 2025-07-24 09:48:18 +03:00
Omar Abdulla 3c70e0ac76 Revert "Bump commit hash of polkadot SDK"
This reverts commit 8aaa69780e.
2025-07-24 09:39:08 +03:00
Omar Abdulla 995ef80940 Revert "Change how the cache key is computed"
This reverts commit 75afdd9cfd.
2025-07-24 09:39:00 +03:00
Omar Abdulla 75afdd9cfd Change how the cache key is computed 2025-07-24 08:31:50 +03:00
Omar Abdulla 8aaa69780e Bump commit hash of polkadot SDK 2025-07-24 08:19:05 +03:00
Omar Abdulla d7795fff9d Pre-seed accounts with more ETH.
This commit fixes and solves some issues around how much ETH we seed an
account with in genesis. Currently, any account that the node has keys
to sign for will be seeded with u128::MAX WEI in genesis. This also
includes the default signer account.
2025-07-24 06:58:46 +03:00
44 changed files with 908 additions and 2093 deletions
-16
View File
@@ -99,12 +99,9 @@ jobs:
- name: Install Geth on Ubuntu
if: matrix.os == 'ubuntu-24.04'
run: |
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install -y protobuf-compiler
sudo apt-get install -y solc
# We were facing some issues in CI with the 1.16.* versions of geth, and specifically on
# Ubuntu. Eventually, we found out that the last version of geth that worked in our CI was
# version 1.15.11. Thus, this is the version that we want to use in CI. The PPA sadly does
@@ -125,22 +122,12 @@ jobs:
wget -qO- "$URL" | sudo tar xz -C /usr/local/bin --strip-components=1
geth --version
curl -sL https://github.com/paritytech/revive/releases/download/v0.3.0/resolc-x86_64-unknown-linux-musl -o resolc
chmod +x resolc
sudo mv resolc /usr/local/bin
- name: Install Geth on macOS
if: matrix.os == 'macos-14'
run: |
brew tap ethereum/ethereum
brew install ethereum protobuf
brew install solidity
curl -sL https://github.com/paritytech/revive/releases/download/v0.3.0/resolc-universal-apple-darwin -o resolc
chmod +x resolc
sudo mv resolc /usr/local/bin
- name: Machete
uses: bnjbvr/cargo-machete@v0.7.1
@@ -156,8 +143,5 @@ jobs:
- name: Check eth-rpc version
run: eth-rpc --version
- name: Check resolc version
run: resolc --version
- name: Test cargo workspace
run: make test
Generated
+8 -100
View File
@@ -339,7 +339,6 @@ dependencies = [
"const-hex",
"derive_more 2.0.1",
"foldhash",
"getrandom 0.3.3",
"hashbrown 0.15.3",
"indexmap 2.10.0",
"itoa",
@@ -2228,66 +2227,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "foundry-compilers-artifacts"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2676d70082ed23680fe2d08c0b750d5f7f2438c6d946f1cb140a76c5e5e0392"
dependencies = [
"foundry-compilers-artifacts-solc",
"foundry-compilers-artifacts-vyper",
]
[[package]]
name = "foundry-compilers-artifacts-solc"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3ada94dc5946334bb08df574855ba345ab03ba8c6f233560c72c8d61fa9db80"
dependencies = [
"alloy-json-abi",
"alloy-primitives",
"foundry-compilers-core",
"path-slash",
"regex",
"semver 1.0.26",
"serde",
"serde_json",
"thiserror 2.0.12",
"tracing",
"yansi",
]
[[package]]
name = "foundry-compilers-artifacts-vyper"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "372052af72652e375a6e7eed22179bd8935114e25e1c5a8cca7f00e8f20bd94c"
dependencies = [
"alloy-json-abi",
"alloy-primitives",
"foundry-compilers-artifacts-solc",
"foundry-compilers-core",
"path-slash",
"semver 1.0.26",
"serde",
]
[[package]]
name = "foundry-compilers-core"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf0962c46855979300f6526ed57f987ccf6a025c2b92ce574b281d9cb2ef666b"
dependencies = [
"alloy-primitives",
"cfg-if",
"dunce",
"path-slash",
"semver 1.0.26",
"serde",
"serde_json",
"thiserror 2.0.12",
]
[[package]]
name = "fs-err"
version = "2.11.0"
@@ -3517,12 +3456,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "path-slash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
[[package]]
name = "pbkdf2"
version = "0.12.2"
@@ -4015,33 +3948,16 @@ dependencies = [
"serde_stacker",
]
[[package]]
name = "revive-dt-common"
version = "0.1.0"
dependencies = [
"anyhow",
"futures",
"once_cell",
"semver 1.0.26",
"tokio",
"tracing",
]
[[package]]
name = "revive-dt-compiler"
version = "0.1.0"
dependencies = [
"alloy",
"alloy-primitives",
"anyhow",
"foundry-compilers-artifacts",
"revive-common",
"revive-dt-common",
"revive-dt-config",
"revive-dt-solc-binaries",
"revive-solc-json-interface",
"semver 1.0.26",
"serde",
"serde_json",
"tracing",
]
@@ -4066,14 +3982,14 @@ dependencies = [
"clap",
"indexmap 2.10.0",
"rayon",
"revive-dt-common",
"revive-dt-compiler",
"revive-dt-config",
"revive-dt-format",
"revive-dt-node",
"revive-dt-node-interaction",
"revive-dt-report",
"semver 1.0.26",
"revive-solc-json-interface",
"serde_json",
"temp-dir",
"tracing",
"tracing-subscriber",
@@ -4087,7 +4003,7 @@ dependencies = [
"alloy-primitives",
"alloy-sol-types",
"anyhow",
"revive-dt-common",
"revive-dt-node-interaction",
"semver 1.0.26",
"serde",
"serde_json",
@@ -4100,9 +4016,7 @@ version = "0.1.0"
dependencies = [
"alloy",
"anyhow",
"revive-dt-common",
"revive-dt-config",
"revive-dt-format",
"revive-dt-node-interaction",
"serde",
"serde_json",
@@ -4119,6 +4033,10 @@ version = "0.1.0"
dependencies = [
"alloy",
"anyhow",
"futures",
"once_cell",
"tokio",
"tracing",
]
[[package]]
@@ -4126,9 +4044,9 @@ name = "revive-dt-report"
version = "0.1.0"
dependencies = [
"anyhow",
"revive-dt-compiler",
"revive-dt-config",
"revive-dt-format",
"revive-solc-json-interface",
"serde",
"serde_json",
"tracing",
@@ -4141,7 +4059,6 @@ dependencies = [
"anyhow",
"hex",
"reqwest",
"revive-dt-common",
"semver 1.0.26",
"serde",
"sha2 0.10.9",
@@ -4239,9 +4156,6 @@ name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
dependencies = [
"rand 0.8.5",
]
[[package]]
name = "rustc-hex"
@@ -6292,12 +6206,6 @@ dependencies = [
"tap",
]
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yoke"
version = "0.8.0"
-2
View File
@@ -11,7 +11,6 @@ repository = "https://github.com/paritytech/revive-differential-testing.git"
rust-version = "1.85.0"
[workspace.dependencies]
revive-dt-common = { version = "0.1.0", path = "crates/common" }
revive-dt-compiler = { version = "0.1.0", path = "crates/compiler" }
revive-dt-config = { version = "0.1.0", path = "crates/config" }
revive-dt-core = { version = "0.1.0", path = "crates/core" }
@@ -26,7 +25,6 @@ alloy-primitives = "1.2.1"
alloy-sol-types = "1.2.1"
anyhow = "1.0"
clap = { version = "4", features = ["derive"] }
foundry-compilers-artifacts = { version = "0.18.0" }
futures = { version = "0.3.31" }
hex = "0.4.3"
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
-17
View File
@@ -1,17 +0,0 @@
[package]
name = "revive-dt-common"
description = "A library containing common concepts that other crates in the workspace can rely on"
version.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow = { workspace = true }
futures = { workspace = true }
semver = { workspace = true }
tracing = { workspace = true }
once_cell = { workspace = true }
tokio = { workspace = true }
-3
View File
@@ -1,3 +0,0 @@
mod blocking_executor;
pub use blocking_executor::*;
-3
View File
@@ -1,3 +0,0 @@
mod files_with_extension_iterator;
pub use files_with_extension_iterator::*;
-7
View File
@@ -1,7 +0,0 @@
//! This crate provides common concepts, functionality, types, macros, and more that other crates in
//! the workspace can benefit from.
pub mod concepts;
pub mod iterators;
pub mod macros;
pub mod types;
-3
View File
@@ -1,3 +0,0 @@
mod define_wrapper_type;
pub use define_wrapper_type::*;
-3
View File
@@ -1,3 +0,0 @@
mod version_or_requirement;
pub use version_or_requirement::*;
@@ -1,41 +0,0 @@
use semver::{Version, VersionReq};
#[derive(Clone, Debug)]
pub enum VersionOrRequirement {
Version(Version),
Requirement(VersionReq),
}
impl From<Version> for VersionOrRequirement {
fn from(value: Version) -> Self {
Self::Version(value)
}
}
impl From<VersionReq> for VersionOrRequirement {
fn from(value: VersionReq) -> Self {
Self::Requirement(value)
}
}
impl TryFrom<VersionOrRequirement> for Version {
type Error = anyhow::Error;
fn try_from(value: VersionOrRequirement) -> Result<Self, Self::Error> {
let VersionOrRequirement::Version(version) = value else {
anyhow::bail!("Version or requirement was not a version");
};
Ok(version)
}
}
impl TryFrom<VersionOrRequirement> for VersionReq {
type Error = anyhow::Error;
fn try_from(value: VersionOrRequirement) -> Result<Self, Self::Error> {
let VersionOrRequirement::Requirement(requirement) = value else {
anyhow::bail!("Version or requirement was not a requirement");
};
Ok(requirement)
}
}
+1 -7
View File
@@ -9,17 +9,11 @@ repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow = { workspace = true }
revive-solc-json-interface = { workspace = true }
revive-dt-common = { workspace = true }
revive-dt-config = { workspace = true }
revive-dt-solc-binaries = { workspace = true }
revive-common = { workspace = true }
alloy = { workspace = true }
alloy-primitives = { workspace = true }
anyhow = { workspace = true }
foundry-compilers-artifacts = { workspace = true }
semver = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
+96 -79
View File
@@ -4,20 +4,20 @@
//! - Polkadot revive Wasm compiler
use std::{
collections::HashMap,
fs::read_to_string,
hash::Hash,
path::{Path, PathBuf},
};
use alloy::json_abi::JsonAbi;
use alloy_primitives::Address;
use semver::Version;
use serde::{Deserialize, Serialize};
use revive_dt_config::Arguments;
use revive_common::EVMVersion;
use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments;
use revive_solc_json_interface::{
SolcStandardJsonInput, SolcStandardJsonInputLanguage, SolcStandardJsonInputSettings,
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection,
SolcStandardJsonOutput,
};
use semver::Version;
pub mod revive_js;
pub mod revive_resolc;
@@ -31,44 +31,63 @@ pub trait SolidityCompiler {
/// The low-level compiler interface.
fn build(
&self,
input: CompilerInput,
additional_options: Self::Options,
) -> anyhow::Result<CompilerOutput>;
input: CompilerInput<Self::Options>,
) -> anyhow::Result<CompilerOutput<Self::Options>>;
fn new(solc_executable: PathBuf) -> Self;
fn get_compiler_executable(
config: &Arguments,
version: impl Into<VersionOrRequirement>,
) -> anyhow::Result<PathBuf>;
fn version(&self) -> anyhow::Result<Version>;
fn get_compiler_executable(config: &Arguments, version: Version) -> anyhow::Result<PathBuf>;
}
/// The generic compilation input configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompilerInput {
pub enable_optimization: Option<bool>,
pub via_ir: Option<bool>,
pub evm_version: Option<EVMVersion>,
#[derive(Debug)]
pub struct CompilerInput<T: PartialEq + Eq + Hash> {
pub extra_options: T,
pub input: SolcStandardJsonInput,
pub allow_paths: Vec<PathBuf>,
pub base_path: Option<PathBuf>,
pub sources: HashMap<PathBuf, String>,
pub libraries: HashMap<PathBuf, HashMap<String, Address>>,
}
/// The generic compilation output configuration.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CompilerOutput {
/// The compiled contracts. The bytecode of the contract is kept as a string incase linking is
/// required and the compiled source has placeholders.
pub contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
#[derive(Debug)]
pub struct CompilerOutput<T: PartialEq + Eq + Hash> {
/// The solc standard JSON input.
pub input: CompilerInput<T>,
/// The produced solc standard JSON output.
pub output: SolcStandardJsonOutput,
/// The error message in case the compiler returns abnormally.
pub error: Option<String>,
}
/// A generic builder style interface for configuring the supported compiler options.
impl<T> PartialEq for CompilerInput<T>
where
T: PartialEq + Eq + Hash,
{
fn eq(&self, other: &Self) -> bool {
let self_input = serde_json::to_vec(&self.input).unwrap_or_default();
let other_input = serde_json::to_vec(&self.input).unwrap_or_default();
self.extra_options.eq(&other.extra_options) && self_input == other_input
}
}
impl<T> Eq for CompilerInput<T> where T: PartialEq + Eq + Hash {}
impl<T> Hash for CompilerInput<T>
where
T: PartialEq + Eq + Hash,
{
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.extra_options.hash(state);
state.write(&serde_json::to_vec(&self.input).unwrap_or_default());
}
}
/// A generic builder style interface for configuring all compiler options.
pub struct Compiler<T: SolidityCompiler> {
input: CompilerInput,
additional_options: T::Options,
input: SolcStandardJsonInput,
extra_options: T::Options,
allow_paths: Vec<PathBuf>,
base_path: Option<PathBuf>,
}
impl Default for Compiler<solc::Solc> {
@@ -83,75 +102,73 @@ where
{
pub fn new() -> Self {
Self {
input: CompilerInput {
enable_optimization: Default::default(),
via_ir: Default::default(),
evm_version: Default::default(),
allow_paths: Default::default(),
base_path: Default::default(),
input: SolcStandardJsonInput {
language: SolcStandardJsonInputLanguage::Solidity,
sources: Default::default(),
libraries: Default::default(),
settings: SolcStandardJsonInputSettings::new(
None,
Default::default(),
None,
SolcStandardJsonInputSettingsSelection::new_required(),
SolcStandardJsonInputSettingsOptimizer::new(
false,
None,
&Version::new(0, 0, 0),
false,
),
None,
None,
),
},
additional_options: T::Options::default(),
extra_options: Default::default(),
allow_paths: Default::default(),
base_path: None,
}
}
pub fn with_optimization(mut self, value: impl Into<Option<bool>>) -> Self {
self.input.enable_optimization = value.into();
pub fn solc_optimizer(mut self, enabled: bool) -> Self {
self.input.settings.optimizer.enabled = enabled;
self
}
pub fn with_via_ir(mut self, value: impl Into<Option<bool>>) -> Self {
self.input.via_ir = value.into();
self
}
pub fn with_evm_version(mut self, version: impl Into<Option<EVMVersion>>) -> Self {
self.input.evm_version = version.into();
self
}
pub fn with_allow_path(mut self, path: impl AsRef<Path>) -> Self {
self.input.allow_paths.push(path.as_ref().into());
self
}
pub fn with_base_path(mut self, path: impl Into<Option<PathBuf>>) -> Self {
self.input.base_path = path.into();
self
}
pub fn with_source(mut self, path: impl AsRef<Path>) -> anyhow::Result<Self> {
pub fn with_source(mut self, path: &Path) -> anyhow::Result<Self> {
self.input
.sources
.insert(path.as_ref().to_path_buf(), read_to_string(path.as_ref())?);
.insert(path.display().to_string(), read_to_string(path)?.into());
Ok(self)
}
pub fn with_library(
mut self,
path: impl AsRef<Path>,
name: impl AsRef<str>,
address: Address,
) -> Self {
self.input
.libraries
.entry(path.as_ref().to_path_buf())
.or_default()
.insert(name.as_ref().into(), address);
pub fn evm_version(mut self, evm_version: EVMVersion) -> Self {
self.input.settings.evm_version = Some(evm_version);
self
}
pub fn with_additional_options(mut self, options: impl Into<T::Options>) -> Self {
self.additional_options = options.into();
pub fn extra_options(mut self, extra_options: T::Options) -> Self {
self.extra_options = extra_options;
self
}
pub fn try_build(self, compiler_path: impl AsRef<Path>) -> anyhow::Result<CompilerOutput> {
T::new(compiler_path.as_ref().to_path_buf()).build(self.input, self.additional_options)
pub fn allow_path(mut self, path: PathBuf) -> Self {
self.allow_paths.push(path);
self
}
pub fn input(&self) -> CompilerInput {
pub fn base_path(mut self, base_path: PathBuf) -> Self {
self.base_path = Some(base_path);
self
}
pub fn try_build(self, solc_path: PathBuf) -> anyhow::Result<CompilerOutput<T::Options>> {
T::new(solc_path).build(CompilerInput {
extra_options: self.extra_options,
input: self.input,
allow_paths: self.allow_paths,
base_path: self.base_path,
})
}
/// Returns the compiler JSON input.
pub fn input(&self) -> SolcStandardJsonInput {
self.input.clone()
}
}
+79 -174
View File
@@ -6,23 +6,9 @@ use std::{
process::{Command, Stdio},
};
use alloy::json_abi::JsonAbi;
use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments;
use revive_solc_json_interface::{
SolcStandardJsonInput, SolcStandardJsonInputLanguage, SolcStandardJsonInputSettings,
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection,
SolcStandardJsonOutput,
};
use crate::{CompilerInput, CompilerOutput, SolidityCompiler};
use anyhow::Context;
use semver::Version;
// TODO: I believe that we need to also pass the solc compiler to resolc so that resolc uses the
// specified solc compiler. I believe that currently we completely ignore the specified solc binary
// when invoking resolc which doesn't seem right if we're using solc as a compiler frontend.
use revive_dt_config::Arguments;
use revive_solc_json_interface::SolcStandardJsonOutput;
/// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode.
#[derive(Debug)]
@@ -37,56 +23,8 @@ impl SolidityCompiler for Resolc {
#[tracing::instrument(level = "debug", ret)]
fn build(
&self,
CompilerInput {
enable_optimization,
// Ignored and not honored since this is required for the resolc compilation.
via_ir: _via_ir,
evm_version,
allow_paths,
base_path,
sources,
libraries,
}: CompilerInput,
additional_options: Self::Options,
) -> anyhow::Result<CompilerOutput> {
let input = SolcStandardJsonInput {
language: SolcStandardJsonInputLanguage::Solidity,
sources: sources
.into_iter()
.map(|(path, source)| (path.display().to_string(), source.into()))
.collect(),
settings: SolcStandardJsonInputSettings {
evm_version,
libraries: Some(
libraries
.into_iter()
.map(|(source_code, libraries_map)| {
(
source_code.display().to_string(),
libraries_map
.into_iter()
.map(|(library_ident, library_address)| {
(library_ident, library_address.to_string())
})
.collect(),
)
})
.collect(),
),
remappings: None,
output_selection: Some(SolcStandardJsonInputSettingsSelection::new_required()),
via_ir: Some(true),
optimizer: SolcStandardJsonInputSettingsOptimizer::new(
enable_optimization.unwrap_or(false),
None,
&Version::new(0, 0, 0),
false,
),
metadata: None,
polkavm: None,
},
};
input: CompilerInput<Self::Options>,
) -> anyhow::Result<CompilerOutput<Self::Options>> {
let mut command = Command::new(&self.resolc_path);
command
.stdin(Stdio::piped())
@@ -94,12 +32,13 @@ impl SolidityCompiler for Resolc {
.stderr(Stdio::piped())
.arg("--standard-json");
if let Some(ref base_path) = base_path {
if let Some(ref base_path) = input.base_path {
command.arg("--base-path").arg(base_path);
}
if !allow_paths.is_empty() {
if !input.allow_paths.is_empty() {
command.arg("--allow-paths").arg(
allow_paths
input
.allow_paths
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
@@ -109,86 +48,93 @@ impl SolidityCompiler for Resolc {
let mut child = command.spawn()?;
let stdin_pipe = child.stdin.as_mut().expect("stdin must be piped");
serde_json::to_writer(stdin_pipe, &input)?;
serde_json::to_writer(stdin_pipe, &input.input)?;
let json_in = serde_json::to_string_pretty(&input.input)?;
let output = child.wait_with_output()?;
let stdout = output.stdout;
let stderr = output.stderr;
if !output.status.success() {
let json_in = serde_json::to_string_pretty(&input)?;
let message = String::from_utf8_lossy(&stderr);
tracing::error!(
status = %output.status,
message = %message,
json_input = json_in,
"Compilation using resolc failed"
"resolc failed exit={} stderr={} JSON-in={} ",
output.status,
&message,
json_in,
);
anyhow::bail!("Compilation failed with an error: {message}");
return Ok(CompilerOutput {
input,
output: Default::default(),
error: Some(message.into()),
});
}
let parsed = serde_json::from_slice::<SolcStandardJsonOutput>(&stdout).map_err(|e| {
anyhow::anyhow!(
"failed to parse resolc JSON output: {e}\nstderr: {}",
String::from_utf8_lossy(&stderr)
)
})?;
let mut parsed =
serde_json::from_slice::<SolcStandardJsonOutput>(&stdout).map_err(|e| {
anyhow::anyhow!(
"failed to parse resolc JSON output: {e}\nstderr: {}",
String::from_utf8_lossy(&stderr)
)
})?;
// Detecting if the compiler output contained errors and reporting them through logs and
// errors instead of returning the compiler output that might contain errors.
for error in parsed.errors.iter().flatten() {
if error.severity == "error" {
tracing::error!(?error, ?input, "Encountered an error in the compilation");
anyhow::bail!("Encountered an error in the compilation: {error}")
}
}
// We need to do some post processing on the output to make it in the same format that solc
// outputs. More specifically, for each contract, the `.metadata` field should be replaced
// with the `.metadata.solc_metadata` field which contains the ABI and other information
// about the compiled contracts. We do this because we do not want any downstream logic to
// need to differentiate between which compiler is being used when extracting the ABI of the
// contracts.
if let Some(ref mut contracts) = parsed.contracts {
for (contract_path, contracts_map) in contracts.iter_mut() {
for (contract_name, contract_info) in contracts_map.iter_mut() {
let Some(metadata) = contract_info.metadata.take() else {
continue;
};
// Get the `solc_metadata` in the metadata of the contract.
let Some(solc_metadata) = metadata
.get("solc_metadata")
.and_then(|metadata| metadata.as_str())
else {
tracing::error!(
contract_path,
contract_name,
metadata = serde_json::to_string(&metadata).unwrap(),
"Encountered a contract compiled with resolc that has no solc_metadata"
);
anyhow::bail!(
"Contract {} compiled with resolc that has no solc_metadata",
contract_name
);
};
// Replace the original metadata with the new solc_metadata.
contract_info.metadata =
Some(serde_json::Value::String(solc_metadata.to_string()));
}
}
}
tracing::debug!(
output = %serde_json::to_string(&parsed).unwrap(),
"Compiled successfully"
);
// Detecting if the compiler output contained errors and reporting them through logs and
// errors instead of returning the compiler output that might contain errors.
for error in parsed.errors.iter().flatten() {
if error.severity == "error" {
tracing::error!(
?error,
?input,
output = %serde_json::to_string(&parsed).unwrap(),
"Encountered an error in the compilation"
);
anyhow::bail!("Encountered an error in the compilation: {error}")
}
}
let Some(contracts) = parsed.contracts else {
anyhow::bail!("Unexpected error - resolc output doesn't have a contracts section");
};
let mut compiler_output = CompilerOutput::default();
for (source_path, contracts) in contracts.into_iter() {
let source_path = PathBuf::from(source_path).canonicalize()?;
let map = compiler_output.contracts.entry(source_path).or_default();
for (contract_name, contract_information) in contracts.into_iter() {
let bytecode = contract_information
.evm
.and_then(|evm| evm.bytecode.clone())
.context("Unexpected - Contract compiled with resolc has no bytecode")?;
let abi = contract_information
.metadata
.as_ref()
.and_then(|metadata| metadata.as_object())
.and_then(|metadata| metadata.get("solc_metadata"))
.and_then(|solc_metadata| solc_metadata.as_str())
.and_then(|metadata| serde_json::from_str::<serde_json::Value>(metadata).ok())
.and_then(|metadata| {
metadata.get("output").and_then(|output| {
output
.get("abi")
.and_then(|abi| serde_json::from_value::<JsonAbi>(abi.clone()).ok())
})
})
.context(
"Unexpected - Failed to get the ABI for a contract compiled with resolc",
)?;
map.insert(contract_name, (bytecode.object, abi));
}
}
Ok(compiler_output)
Ok(CompilerOutput {
input,
output: parsed,
error: None,
})
}
fn new(resolc_path: PathBuf) -> Self {
@@ -197,7 +143,7 @@ impl SolidityCompiler for Resolc {
fn get_compiler_executable(
config: &Arguments,
_version: impl Into<VersionOrRequirement>,
_version: semver::Version,
) -> anyhow::Result<PathBuf> {
if !config.resolc.as_os_str().is_empty() {
return Ok(config.resolc.clone());
@@ -205,45 +151,4 @@ impl SolidityCompiler for Resolc {
Ok(PathBuf::from("resolc"))
}
fn version(&self) -> anyhow::Result<semver::Version> {
// Logic for parsing the resolc version from the following string:
// Solidity frontend for the revive compiler version 0.3.0+commit.b238913.llvm-18.1.8
let output = Command::new(self.resolc_path.as_path())
.arg("--version")
.stdout(Stdio::piped())
.spawn()?
.wait_with_output()?
.stdout;
let output = String::from_utf8_lossy(&output);
let version_string = output
.split("version ")
.nth(1)
.context("Version parsing failed")?
.split("+")
.next()
.context("Version parsing failed")?;
Version::parse(version_string).map_err(Into::into)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn compiler_version_can_be_obtained() {
// Arrange
let args = Arguments::default();
let path = Resolc::get_compiler_executable(&args, Version::new(0, 7, 6)).unwrap();
let compiler = Resolc::new(path);
// Act
let version = compiler.version();
// Assert
let _ = version.expect("Failed to get version");
}
}
+30 -162
View File
@@ -6,21 +6,10 @@ use std::{
process::{Command, Stdio},
};
use revive_dt_common::types::VersionOrRequirement;
use crate::{CompilerInput, CompilerOutput, SolidityCompiler};
use revive_dt_config::Arguments;
use revive_dt_solc_binaries::download_solc;
use crate::{CompilerInput, CompilerOutput, SolidityCompiler};
use anyhow::Context;
use foundry_compilers_artifacts::{
output_selection::{
BytecodeOutputSelection, ContractOutputSelection, EvmOutputSelection, OutputSelection,
},
solc::CompilerOutput as SolcOutput,
solc::*,
};
use semver::Version;
use revive_solc_json_interface::SolcStandardJsonOutput;
#[derive(Debug)]
pub struct Solc {
@@ -33,63 +22,8 @@ impl SolidityCompiler for Solc {
#[tracing::instrument(level = "debug", ret)]
fn build(
&self,
CompilerInput {
enable_optimization,
via_ir,
evm_version,
allow_paths,
base_path,
sources,
libraries,
}: CompilerInput,
_: Self::Options,
) -> anyhow::Result<CompilerOutput> {
let input = SolcInput {
language: SolcLanguage::Solidity,
sources: Sources(
sources
.into_iter()
.map(|(source_path, source_code)| (source_path, Source::new(source_code)))
.collect(),
),
settings: Settings {
optimizer: Optimizer {
enabled: enable_optimization,
details: Some(Default::default()),
..Default::default()
},
output_selection: OutputSelection::common_output_selection(
[
ContractOutputSelection::Abi,
ContractOutputSelection::Evm(EvmOutputSelection::ByteCode(
BytecodeOutputSelection::Object,
)),
]
.into_iter()
.map(|item| item.to_string()),
),
evm_version: evm_version.map(|version| version.to_string().parse().unwrap()),
via_ir,
libraries: Libraries {
libs: libraries
.into_iter()
.map(|(file_path, libraries)| {
(
file_path,
libraries
.into_iter()
.map(|(library_name, library_address)| {
(library_name, library_address.to_string())
})
.collect(),
)
})
.collect(),
},
..Default::default()
},
};
input: CompilerInput<Self::Options>,
) -> anyhow::Result<CompilerOutput<Self::Options>> {
let mut command = Command::new(&self.solc_path);
command
.stdin(Stdio::piped())
@@ -97,12 +31,13 @@ impl SolidityCompiler for Solc {
.stderr(Stdio::piped())
.arg("--standard-json");
if let Some(ref base_path) = base_path {
if let Some(ref base_path) = input.base_path {
command.arg("--base-path").arg(base_path);
}
if !allow_paths.is_empty() {
if !input.allow_paths.is_empty() {
command.arg("--allow-paths").arg(
allow_paths
input
.allow_paths
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
@@ -112,32 +47,31 @@ impl SolidityCompiler for Solc {
let mut child = command.spawn()?;
let stdin = child.stdin.as_mut().expect("should be piped");
serde_json::to_writer(stdin, &input)?;
serde_json::to_writer(stdin, &input.input)?;
let output = child.wait_with_output()?;
if !output.status.success() {
let json_in = serde_json::to_string_pretty(&input)?;
let message = String::from_utf8_lossy(&output.stderr);
tracing::error!(
status = %output.status,
message = %message,
json_input = json_in,
"Compilation using solc failed"
);
anyhow::bail!("Compilation failed with an error: {message}");
tracing::error!("solc failed exit={} stderr={}", output.status, &message);
return Ok(CompilerOutput {
input,
output: Default::default(),
error: Some(message.into()),
});
}
let parsed = serde_json::from_slice::<SolcOutput>(&output.stdout).map_err(|e| {
anyhow::anyhow!(
"failed to parse resolc JSON output: {e}\nstderr: {}",
String::from_utf8_lossy(&output.stdout)
)
})?;
let parsed =
serde_json::from_slice::<SolcStandardJsonOutput>(&output.stdout).map_err(|e| {
anyhow::anyhow!(
"failed to parse resolc JSON output: {e}\nstderr: {}",
String::from_utf8_lossy(&output.stdout)
)
})?;
// Detecting if the compiler output contained errors and reporting them through logs and
// errors instead of returning the compiler output that might contain errors.
for error in parsed.errors.iter() {
if error.severity == Severity::Error {
for error in parsed.errors.iter().flatten() {
if error.severity == "error" {
tracing::error!(?error, ?input, "Encountered an error in the compilation");
anyhow::bail!("Encountered an error in the compilation: {error}")
}
@@ -148,29 +82,11 @@ impl SolidityCompiler for Solc {
"Compiled successfully"
);
let mut compiler_output = CompilerOutput::default();
for (contract_path, contracts) in parsed.contracts {
let map = compiler_output
.contracts
.entry(contract_path.canonicalize()?)
.or_default();
for (contract_name, contract_info) in contracts.into_iter() {
let source_code = contract_info
.evm
.and_then(|evm| evm.bytecode)
.map(|bytecode| match bytecode.object {
BytecodeObject::Bytecode(bytecode) => bytecode.to_string(),
BytecodeObject::Unlinked(unlinked) => unlinked,
})
.context("Unexpected - contract compiled with solc has no source code")?;
let abi = contract_info
.abi
.context("Unexpected - contract compiled with solc as no ABI")?;
map.insert(contract_name, (source_code, abi));
}
}
Ok(compiler_output)
Ok(CompilerOutput {
input,
output: parsed,
error: None,
})
}
fn new(solc_path: PathBuf) -> Self {
@@ -179,57 +95,9 @@ impl SolidityCompiler for Solc {
fn get_compiler_executable(
config: &Arguments,
version: impl Into<VersionOrRequirement>,
version: semver::Version,
) -> anyhow::Result<PathBuf> {
let path = download_solc(config.directory(), version, config.wasm)?;
Ok(path)
}
fn version(&self) -> anyhow::Result<semver::Version> {
// The following is the parsing code for the version from the solc version strings which
// look like the following:
// ```
// solc, the solidity compiler commandline interface
// Version: 0.8.30+commit.73712a01.Darwin.appleclang
// ```
let child = Command::new(self.solc_path.as_path())
.arg("--version")
.stdout(Stdio::piped())
.spawn()?;
let output = child.wait_with_output()?;
let output = String::from_utf8_lossy(&output.stdout);
let version_line = output
.split("Version: ")
.nth(1)
.context("Version parsing failed")?;
let version_string = version_line
.split("+")
.next()
.context("Version parsing failed")?;
Version::parse(version_string).map_err(Into::into)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn compiler_version_can_be_obtained() {
// Arrange
let args = Arguments::default();
let path = Solc::get_compiler_executable(&args, Version::new(0, 7, 6)).unwrap();
let compiler = Solc::new(path);
// Act
let version = compiler.version();
// Assert
assert_eq!(
version.expect("Failed to get version"),
Version::new(0, 7, 6)
)
}
}
@@ -1,9 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.9;
contract Callable {
function f(uint[1] memory p1) public pure returns(uint) {
return p1[0];
}
}
@@ -1,13 +0,0 @@
// SPDX-License-Identifier: MIT
// Report https://linear.app/matterlabs/issue/CPR-269/call-with-calldata-variable-bug
pragma solidity >=0.6.9;
import "./callable.sol";
contract Main {
function main(uint[1] calldata p1, Callable callable) public returns(uint) {
return callable.f(p1);
}
}
@@ -1,21 +0,0 @@
{ "cases": [ {
"name": "first",
"inputs": [
{
"instance": "Main",
"method": "main",
"calldata": [
"1",
"Callable.address"
]
}
],
"expected": [
"1"
]
} ],
"contracts": {
"Main": "main.sol:Main",
"Callable": "callable.sol:Callable"
}
}
-81
View File
@@ -1,81 +0,0 @@
use std::path::PathBuf;
use revive_dt_compiler::{Compiler, SolidityCompiler, revive_resolc::Resolc, solc::Solc};
use revive_dt_config::Arguments;
use semver::Version;
#[test]
fn contracts_can_be_compiled_with_solc() {
// Arrange
let args = Arguments::default();
let compiler_path = Solc::get_compiler_executable(&args, Version::new(0, 8, 30)).unwrap();
// Act
let output = Compiler::<Solc>::new()
.with_source("./tests/assets/array_one_element/callable.sol")
.unwrap()
.with_source("./tests/assets/array_one_element/main.sol")
.unwrap()
.try_build(compiler_path);
// Assert
let output = output.expect("Failed to compile");
assert_eq!(output.contracts.len(), 2);
let main_file_contracts = output
.contracts
.get(
&PathBuf::from("./tests/assets/array_one_element/main.sol")
.canonicalize()
.unwrap(),
)
.unwrap();
let callable_file_contracts = output
.contracts
.get(
&PathBuf::from("./tests/assets/array_one_element/callable.sol")
.canonicalize()
.unwrap(),
)
.unwrap();
assert!(main_file_contracts.contains_key("Main"));
assert!(callable_file_contracts.contains_key("Callable"));
}
#[test]
fn contracts_can_be_compiled_with_resolc() {
// Arrange
let args = Arguments::default();
let compiler_path = Resolc::get_compiler_executable(&args, Version::new(0, 8, 30)).unwrap();
// Act
let output = Compiler::<Resolc>::new()
.with_source("./tests/assets/array_one_element/callable.sol")
.unwrap()
.with_source("./tests/assets/array_one_element/main.sol")
.unwrap()
.try_build(compiler_path);
// Assert
let output = output.expect("Failed to compile");
assert_eq!(output.contracts.len(), 2);
let main_file_contracts = output
.contracts
.get(
&PathBuf::from("./tests/assets/array_one_element/main.sol")
.canonicalize()
.unwrap(),
)
.unwrap();
let callable_file_contracts = output
.contracts
.get(
&PathBuf::from("./tests/assets/array_one_element/callable.sol")
.canonicalize()
.unwrap(),
)
.unwrap();
assert!(main_file_contracts.contains_key("Main"));
assert!(callable_file_contracts.contains_key("Callable"));
}
+2 -10
View File
@@ -3,7 +3,6 @@
use std::{
fmt::Display,
path::{Path, PathBuf},
sync::LazyLock,
};
use alloy::{network::EthereumWallet, signers::local::PrivateKeySigner};
@@ -55,7 +54,7 @@ pub struct Arguments {
pub geth: PathBuf,
/// The maximum time in milliseconds to wait for geth to start.
#[arg(long = "geth-start-timeout", default_value = "5000")]
#[arg(long = "geth-start-timeout", default_value = "2000")]
pub geth_start_timeout: u64,
/// The test network chain ID.
@@ -145,14 +144,7 @@ impl Arguments {
impl Default for Arguments {
fn default() -> Self {
static TEMP_DIR: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
let default = Arguments::parse_from(["retester"]);
Arguments {
temp_dir: Some(&TEMP_DIR),
..default
}
Arguments::parse_from(["retester"])
}
}
+2 -2
View File
@@ -13,7 +13,6 @@ name = "retester"
path = "src/main.rs"
[dependencies]
revive-dt-common = { workspace = true }
revive-dt-compiler = { workspace = true }
revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true }
@@ -28,5 +27,6 @@ indexmap = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
rayon = { workspace = true }
semver = { workspace = true }
revive-solc-json-interface = { workspace = true }
serde_json = { workspace = true }
temp-dir = { workspace = true }
+190 -394
View File
@@ -1,13 +1,10 @@
//! The test driver handles the compilation and execution of the test cases.
use std::collections::HashMap;
use std::fmt::Debug;
use std::marker::PhantomData;
use std::path::PathBuf;
use alloy::json_abi::JsonAbi;
use alloy::network::{Ethereum, TransactionBuilder};
use alloy::primitives::U256;
use alloy::rpc::types::TransactionReceipt;
use alloy::rpc::types::trace::geth::{
CallFrame, GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingOptions, GethTrace,
@@ -22,20 +19,21 @@ use alloy::{
};
use anyhow::Context;
use indexmap::IndexMap;
use semver::Version;
use revive_dt_common::iterators::FilesWithExtensionIterator;
use revive_dt_compiler::{Compiler, SolidityCompiler};
use revive_dt_config::Arguments;
use revive_dt_format::case::CaseIdx;
use revive_dt_format::input::{Calldata, EtherValue, Expected, ExpectedOutput, Method};
use revive_dt_format::metadata::{ContractInstance, ContractPathAndIdent};
use revive_dt_format::input::{Calldata, Expected, ExpectedOutput, Method};
use revive_dt_format::metadata::{ContractInstance, ContractPathAndIdentifier};
use revive_dt_format::{input::Input, metadata::Metadata, mode::SolcMode};
use revive_dt_node::Node;
use revive_dt_node_interaction::EthereumNode;
use revive_dt_report::reporter::{CompilationTask, Report, Span};
use revive_solc_json_interface::SolcStandardJsonOutput;
use serde_json::Value;
use std::fmt::Debug;
use crate::Platform;
use crate::common::*;
pub struct State<'a, T: Platform> {
/// The configuration that the framework was started with.
@@ -47,27 +45,17 @@ pub struct State<'a, T: Platform> {
/// The [`Span`] used in reporting.
span: Span,
/// A map of all of the compiled contracts for the given metadata file.
compiled_contracts: HashMap<PathBuf, HashMap<String, (String, JsonAbi)>>,
/// A vector of all of the compiled contracts. Each call to [`build_contracts`] adds a new entry
/// to this vector.
///
/// [`build_contracts`]: State::build_contracts
contracts: Vec<SolcStandardJsonOutput>,
/// This map stores the contracts deployments that have been made for each case within a
/// metadata file. Note, this means that the state can't be reused between different metadata
/// files.
deployed_contracts: HashMap<CaseIdx, HashMap<ContractInstance, (Address, JsonAbi)>>,
/// This map stores the variables used for each one of the cases contained in the metadata
/// file.
variables: HashMap<CaseIdx, HashMap<String, U256>>,
/// This is a map of the deployed libraries.
///
/// This map is not per case, but rather, per metadata file. This means that we do not redeploy
/// the libraries with each case.
deployed_libraries: HashMap<ContractInstance, (Address, JsonAbi)>,
/// Stores the version of the compiler used for the given Solc mode.
compiler_version: HashMap<&'a SolcMode, Version>,
phantom: PhantomData<T>,
}
@@ -79,11 +67,8 @@ where
Self {
config,
span,
compiled_contracts: Default::default(),
contracts: Default::default(),
deployed_contracts: Default::default(),
variables: Default::default(),
deployed_libraries: Default::default(),
compiler_version: Default::default(),
phantom: Default::default(),
}
}
@@ -93,11 +78,7 @@ where
self.span
}
pub fn build_contracts(
&mut self,
mode: &'a SolcMode,
metadata: &Metadata,
) -> anyhow::Result<()> {
pub fn build_contracts(&mut self, mode: &SolcMode, metadata: &Metadata) -> anyhow::Result<()> {
let mut span = self.span();
span.next_metadata(
metadata
@@ -107,66 +88,44 @@ where
.clone(),
);
let compiler_version_or_requirement =
mode.compiler_version_to_use(self.config.solc.clone());
let compiler_path =
T::Compiler::get_compiler_executable(self.config, compiler_version_or_requirement)?;
let compiler_version = T::Compiler::new(compiler_path.clone()).version()?;
self.compiler_version.insert(mode, compiler_version.clone());
tracing::info!(%compiler_version, "Resolved the compiler version to use");
let Some(version) = mode.last_patch_version(&self.config.solc) else {
anyhow::bail!("unsupported solc version: {:?}", &mode.solc_version);
};
let compiler = Compiler::<T::Compiler>::new()
.with_allow_path(metadata.directory()?)
.with_optimization(mode.solc_optimize());
let mut compiler = metadata
.files_to_compile()?
.try_fold(compiler, |compiler, path| compiler.with_source(&path))?;
for (library_instance, (library_address, _)) in self.deployed_libraries.iter() {
let library_ident = &metadata
.contracts
.as_ref()
.and_then(|contracts| contracts.get(library_instance))
.expect("Impossible for library to not be found in contracts")
.contract_ident;
.allow_path(metadata.directory()?)
.solc_optimizer(mode.solc_optimize());
// Note the following: we need to tell solc which files require the libraries to be
// linked into them. We do not have access to this information and therefore we choose
// an easier, yet more compute intensive route, of telling solc that all of the files
// need to link the library and it will only perform the linking for the files that do
// actually need the library.
compiler = FilesWithExtensionIterator::new(metadata.directory()?)
.with_allowed_extension("sol")
.fold(compiler, |compiler, path| {
compiler.with_library(&path, library_ident.as_str(), *library_address)
});
}
let compiler = FilesWithExtensionIterator::new(metadata.directory()?)
.with_allowed_extension("sol")
.try_fold(compiler, |compiler, path| compiler.with_source(&path))?;
let mut task = CompilationTask {
json_input: compiler.input(),
json_output: None,
mode: mode.clone(),
compiler_version: format!("{}", &compiler_version),
compiler_version: format!("{}", &version),
error: None,
};
let compiler_path = T::Compiler::get_compiler_executable(self.config, version)?;
match compiler.try_build(compiler_path) {
Ok(output) => {
task.json_output = Some(output.clone());
task.json_output = Some(output.output.clone());
task.error = output.error;
self.contracts.push(output.output);
for (contract_path, contracts) in output.contracts.into_iter() {
let map = self
.compiled_contracts
.entry(contract_path.clone())
.or_default();
for (contract_name, contract_info) in contracts.into_iter() {
tracing::debug!(
contract_path = %contract_path.display(),
contract_name = contract_name,
"Compiled contract"
);
map.insert(contract_name, contract_info);
if let Some(last_output) = self.contracts.last() {
if let Some(contracts) = &last_output.contracts {
for (file, contracts_map) in contracts {
for contract_name in contracts_map.keys() {
tracing::debug!(
"Compiled contract: {contract_name} from file: {file}"
);
}
}
} else {
tracing::warn!("Compiled contracts field is None");
}
}
@@ -181,56 +140,18 @@ where
}
}
pub fn build_and_publish_libraries(
&mut self,
metadata: &Metadata,
mode: &'a SolcMode,
node: &T::Blockchain,
) -> anyhow::Result<()> {
self.build_contracts(mode, metadata)?;
for library_instance in metadata
.libraries
.iter()
.flatten()
.flat_map(|(_, map)| map.values())
{
self.get_or_deploy_contract_instance(
library_instance,
metadata,
None,
Input::default_caller(),
None,
None,
node,
)?;
}
Ok(())
}
pub fn handle_input(
&mut self,
metadata: &Metadata,
case_idx: CaseIdx,
input: &Input,
node: &T::Blockchain,
mode: &SolcMode,
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
let deployment_receipts =
self.handle_contract_deployment(metadata, case_idx, input, node)?;
let execution_receipt =
self.handle_input_execution(case_idx, input, deployment_receipts, node)?;
let tracing_result = self.handle_input_call_frame_tracing(&execution_receipt, node)?;
self.handle_input_variable_assignment(case_idx, input, &tracing_result)?;
self.handle_input_expectations(
case_idx,
input,
&execution_receipt,
node,
mode,
&tracing_result,
)?;
self.handle_input_expectations(case_idx, input, &execution_receipt, node)?;
self.handle_input_diff(case_idx, execution_receipt, node)
}
@@ -254,7 +175,7 @@ where
if !self
.deployed_contracts
.entry(case_idx)
.or_insert_with(|| self.deployed_libraries.clone())
.or_default()
.contains_key(&instance)
{
instances_we_must_deploy.entry(instance).or_insert(false);
@@ -272,22 +193,124 @@ 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(&input.calldata);
let value = deploy_with_constructor_arguments
.then_some(input.value)
.flatten();
// What we have at this moment is just a contract instance which is kind of like a variable
// name for an actual underlying contract. So, we need to resolve this instance to the info
// of the contract that it belongs to.
let Some(ContractPathAndIdentifier {
contract_source_path,
contract_ident,
}) = metadata.contract_sources()?.remove(&instance)
else {
tracing::error!("Contract source not found for instance");
anyhow::bail!("Contract source not found for instance {:?}", instance)
};
if let (_, _, Some(receipt)) = self.get_or_deploy_contract_instance(
&instance,
metadata,
case_idx,
input.caller,
calldata,
value,
node,
)? {
receipts.insert(instance.clone(), receipt);
let compiled_contract = self.contracts.iter().find_map(|output| {
output
.contracts
.as_ref()?
.get(&contract_source_path.display().to_string())
.and_then(|source_file_contracts| {
source_file_contracts.get(contract_ident.as_ref())
})
});
let Some(code) = compiled_contract
.and_then(|contract| contract.evm.as_ref().and_then(|evm| evm.bytecode.as_ref()))
else {
tracing::error!(
contract_source_path = contract_source_path.display().to_string(),
contract_ident = contract_ident.as_ref(),
"Failed to find bytecode for contract"
);
anyhow::bail!("Failed to find bytecode for contract {:?}", instance)
};
// TODO: When we want to do linking it would be best to do it at this stage here. We have
// the context from the metadata files and therefore know what needs to be linked and in
// what order it needs to happen.
let mut code = match alloy::hex::decode(&code.object) {
Ok(code) => code,
Err(error) => {
tracing::error!(
?error,
contract_source_path = contract_source_path.display().to_string(),
contract_ident = contract_ident.as_ref(),
"Failed to hex-decode byte code - This could possibly mean that the bytecode requires linking"
);
anyhow::bail!("Failed to hex-decode the byte code {}", error)
}
};
if deploy_with_constructor_arguments {
let encoded_input = input
.encoded_input(self.deployed_contracts.entry(case_idx).or_default(), node)?;
code.extend(encoded_input.to_vec());
}
let tx = {
let tx = TransactionRequest::default().from(input.caller);
let tx = match input.value {
Some(ref value) if deploy_with_constructor_arguments => {
tx.value(value.into_inner())
}
_ => tx,
};
TransactionBuilder::<Ethereum>::with_deploy_code(tx, code)
};
let receipt = match node.execute_transaction(tx) {
Ok(receipt) => receipt,
Err(error) => {
tracing::error!(
node = std::any::type_name::<T>(),
?error,
"Contract deployment transaction failed."
);
return Err(error);
}
};
let Some(address) = receipt.contract_address else {
tracing::error!("Contract deployment transaction didn't return an address");
anyhow::bail!("Contract deployment didn't return an address");
};
tracing::info!(
instance_name = ?instance,
instance_address = ?address,
"Deployed contract"
);
let Some(Value::String(metadata)) =
compiled_contract.and_then(|contract| contract.metadata.as_ref())
else {
tracing::error!("Contract does not have a metadata field");
anyhow::bail!("Contract does not have a metadata field");
};
let Ok(metadata) = serde_json::from_str::<Value>(metadata) else {
tracing::error!(%metadata, "Failed to parse solc metadata into a structured value");
anyhow::bail!("Failed to parse solc metadata into a structured value {metadata}");
};
let Some(abi) = metadata.get("output").and_then(|value| value.get("abi")) else {
tracing::error!(%metadata, "Failed to access the .output.abi field of the solc metadata");
anyhow::bail!(
"Failed to access the .output.abi field of the solc metadata {metadata}"
);
};
let Ok(abi) = serde_json::from_value::<JsonAbi>(abi.clone()) else {
tracing::error!(%metadata, "Failed to deserialize ABI into a structured format");
anyhow::bail!("Failed to deserialize ABI into a structured format {metadata}");
};
self.deployed_contracts
.entry(case_idx)
.or_default()
.insert(instance.clone(), (address, abi));
receipts.insert(instance.clone(), receipt);
}
Ok(receipts)
@@ -308,13 +331,9 @@ where
.remove(&input.instance)
.context("Failed to find deployment receipt"),
Method::Fallback | Method::FunctionName(_) => {
let tx = match input.legacy_transaction(
self.deployed_contracts
.entry(case_idx)
.or_insert_with(|| self.deployed_libraries.clone()),
&*self.variables.entry(case_idx).or_default(),
node,
) {
let tx = match input
.legacy_transaction(self.deployed_contracts.entry(case_idx).or_default(), node)
{
Ok(tx) => {
tracing::debug!("Legacy transaction data: {tx:#?}");
tx
@@ -342,64 +361,12 @@ where
}
}
fn handle_input_call_frame_tracing(
&self,
execution_receipt: &TransactionReceipt,
node: &T::Blockchain,
) -> anyhow::Result<CallFrame> {
node.trace_transaction(
execution_receipt,
GethDebugTracingOptions {
tracer: Some(GethDebugTracerType::BuiltInTracer(
GethDebugBuiltInTracerType::CallTracer,
)),
..Default::default()
},
)
.map(|trace| {
trace
.try_into_call_frame()
.expect("Impossible - we requested a callframe trace so we must get it back")
})
}
fn handle_input_variable_assignment(
&mut self,
case_idx: CaseIdx,
input: &Input,
tracing_result: &CallFrame,
) -> anyhow::Result<()> {
let Some(ref assignments) = input.variable_assignments else {
return Ok(());
};
// Handling the return data variable assignments.
for (variable_name, output_word) in assignments.return_data.iter().zip(
tracing_result
.output
.as_ref()
.unwrap_or_default()
.to_vec()
.chunks(32),
) {
let value = U256::from_be_slice(output_word);
self.variables
.entry(case_idx)
.or_default()
.insert(variable_name.clone(), value);
}
Ok(())
}
fn handle_input_expectations(
&mut self,
case_idx: CaseIdx,
input: &Input,
execution_receipt: &TransactionReceipt,
node: &T::Blockchain,
mode: &SolcMode,
tracing_result: &CallFrame,
) -> anyhow::Result<()> {
let span = tracing::info_span!("Handling input expectations");
let _guard = span.enter();
@@ -432,14 +399,29 @@ where
}
}
// Note: we need to do assertions and checks on the output of the last call and this isn't
// available in the receipt. The only way to get this information is through tracing on the
// node.
let tracing_result = node
.trace_transaction(
execution_receipt,
GethDebugTracingOptions {
tracer: Some(GethDebugTracerType::BuiltInTracer(
GethDebugBuiltInTracerType::CallTracer,
)),
..Default::default()
},
)?
.try_into_call_frame()
.expect("Impossible - we requested a callframe trace so we must get it back");
for expectation in expectations.iter() {
self.handle_input_expectation_item(
case_idx,
execution_receipt,
node,
expectation,
tracing_result,
mode,
&tracing_result,
)?;
}
@@ -453,36 +435,20 @@ where
node: &T::Blockchain,
expectation: &ExpectedOutput,
tracing_result: &CallFrame,
mode: &SolcMode,
) -> anyhow::Result<()> {
if let Some(ref version_requirement) = expectation.compiler_version {
let compiler_version = self
.compiler_version
.get(mode)
.context("Failed to find the compiler version fo the solc mode")?;
if !version_requirement.matches(compiler_version) {
return Ok(());
}
}
// TODO: We want to respect the compiler version filter on the expected output but would
// require some changes to the interfaces of the compiler and such. So, we add it later.
// Additionally, what happens if the compiler filter doesn't match? Do we consider that the
// transaction should succeed? Do we just ignore the expectation?
let deployed_contracts = self
.deployed_contracts
.entry(case_idx)
.or_insert_with(|| self.deployed_libraries.clone());
let variables = self.variables.entry(case_idx).or_default();
let deployed_contracts = self.deployed_contracts.entry(case_idx).or_default();
let chain_state_provider = node;
// Handling the receipt state assertion.
let expected = !expectation.exception;
let actual = execution_receipt.status();
if actual != expected {
tracing::error!(
expected,
actual,
?execution_receipt,
?tracing_result,
"Transaction status assertion failed"
);
tracing::error!(expected, actual, "Transaction status assertion failed",);
anyhow::bail!(
"Transaction status assertion failed - Expected {expected} but got {actual}",
);
@@ -492,12 +458,7 @@ where
if let Some(ref expected_calldata) = expectation.return_data {
let expected = expected_calldata;
let actual = &tracing_result.output.as_ref().unwrap_or_default();
if !expected.is_equivalent(
actual,
deployed_contracts,
&*variables,
chain_state_provider,
)? {
if !expected.is_equivalent(actual, deployed_contracts, chain_state_provider)? {
tracing::error!(
?execution_receipt,
?expected,
@@ -525,13 +486,8 @@ where
expected_events.iter().zip(execution_receipt.logs())
{
// Handling the emitter assertion.
if let Some(ref expected_address) = expected_event.address {
let expected = Address::from_slice(
Calldata::new_compound([expected_address])
.calldata(deployed_contracts, &*variables, node)?
.get(12..32)
.expect("Can't fail"),
);
if let Some(expected_address) = expected_event.address {
let expected = expected_address;
let actual = actual_event.address();
if actual != expected {
tracing::error!(
@@ -552,11 +508,10 @@ where
.iter()
.zip(actual_event.topics())
{
let expected = Calldata::new_compound([expected]);
let expected = Calldata::Compound(vec![expected.clone()]);
if !expected.is_equivalent(
&actual.0,
deployed_contracts,
&*variables,
chain_state_provider,
)? {
tracing::error!(
@@ -574,12 +529,7 @@ where
// Handling the values assertion.
let expected = &expected_event.values;
let actual = &actual_event.data().data;
if !expected.is_equivalent(
&actual.0,
deployed_contracts,
&*variables,
chain_state_provider,
)? {
if !expected.is_equivalent(&actual.0, deployed_contracts, chain_state_provider)? {
tracing::error!(
?execution_receipt,
?expected,
@@ -616,119 +566,6 @@ where
Ok((execution_receipt, trace, diff))
}
/// Gets the information of a deployed contract or library from the state. If it's found to not
/// be deployed then it will be deployed.
///
/// If a [`CaseIdx`] is not specified then this contact instance address will be stored in the
/// cross-case deployed contracts address mapping.
#[allow(clippy::too_many_arguments)]
pub fn get_or_deploy_contract_instance(
&mut self,
contract_instance: &ContractInstance,
metadata: &Metadata,
case_idx: impl Into<Option<CaseIdx>>,
deployer: Address,
calldata: Option<&Calldata>,
value: Option<EtherValue>,
node: &T::Blockchain,
) -> anyhow::Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
let case_idx = case_idx.into();
let deployed_contracts = match case_idx {
Some(case_idx) => self
.deployed_contracts
.entry(case_idx)
.or_insert_with(|| self.deployed_libraries.clone()),
None => &mut self.deployed_libraries,
};
if let Some((address, abi)) = deployed_contracts.get(contract_instance) {
return Ok((*address, abi.clone(), None));
}
let Some(ContractPathAndIdent {
contract_source_path,
contract_ident,
}) = metadata.contract_sources()?.remove(contract_instance)
else {
tracing::error!("Contract source not found for instance");
anyhow::bail!(
"Contract source not found for instance {:?}",
contract_instance
)
};
let Some((code, abi)) = self
.compiled_contracts
.get(&contract_source_path)
.and_then(|source_file_contracts| source_file_contracts.get(contract_ident.as_ref()))
.cloned()
else {
tracing::error!(
contract_source_path = contract_source_path.display().to_string(),
contract_ident = contract_ident.as_ref(),
"Failed to find information for contract"
);
anyhow::bail!(
"Failed to find information for contract {:?}",
contract_instance
)
};
let mut code = match alloy::hex::decode(&code) {
Ok(code) => code,
Err(error) => {
tracing::error!(
?error,
contract_source_path = contract_source_path.display().to_string(),
contract_ident = contract_ident.as_ref(),
"Failed to hex-decode byte code - This could possibly mean that the bytecode requires linking"
);
anyhow::bail!("Failed to hex-decode the byte code {}", error)
}
};
if let Some(calldata) = calldata {
let calldata = calldata.calldata(deployed_contracts, None, node)?;
code.extend(calldata);
}
let tx = {
let tx = TransactionRequest::default().from(deployer);
let tx = match value {
Some(ref value) => tx.value(value.into_inner()),
_ => tx,
};
TransactionBuilder::<Ethereum>::with_deploy_code(tx, code)
};
let receipt = match node.execute_transaction(tx) {
Ok(receipt) => receipt,
Err(error) => {
tracing::error!(
node = std::any::type_name::<T>(),
?error,
"Contract deployment transaction failed."
);
return Err(error);
}
};
let Some(address) = receipt.contract_address else {
tracing::error!("Contract deployment transaction didn't return an address");
anyhow::bail!("Contract deployment didn't return an address");
};
tracing::info!(
instance_name = ?contract_instance,
instance_address = ?address,
"Deployed contract"
);
deployed_contracts.insert(contract_instance.clone(), (address, abi.clone()));
Ok((address, abi, Some(receipt)))
}
}
pub struct Driver<'a, Leader: Platform, Follower: Platform> {
@@ -835,42 +672,6 @@ where
let mut leader_state = State::<L>::new(self.config, span);
let mut follower_state = State::<F>::new(self.config, span);
// Note: we are currently forced to do two compilation passes due to linking. In the
// first compilation pass we compile the libraries and publish them to the chain. In the
// second compilation pass we compile the contracts with the library addresses so that
// they're linked at compile-time.
let build_result = tracing::info_span!("Building and publishing libraries")
.in_scope(|| {
match leader_state.build_and_publish_libraries(self.metadata, &mode, self.leader_node) {
Ok(_) => {
tracing::debug!(target = ?Target::Leader, "Library building succeeded");
execution_result.add_successful_build(Target::Leader, mode.clone());
},
Err(error) => {
tracing::error!(target = ?Target::Leader, ?error, "Library building failed");
execution_result.add_failed_build(Target::Leader, mode.clone(), error);
return Err(());
}
}
match follower_state.build_and_publish_libraries(self.metadata, &mode, self.follower_node) {
Ok(_) => {
tracing::debug!(target = ?Target::Follower, "Library building succeeded");
execution_result.add_successful_build(Target::Follower, mode.clone());
},
Err(error) => {
tracing::error!(target = ?Target::Follower, ?error, "Library building failed");
execution_result.add_failed_build(Target::Follower, mode.clone(), error);
return Err(());
}
}
Ok(())
});
if build_result.is_err() {
// Note: We skip to the next solc mode as there's nothing that we can do at this
// point, the building has failed. We do NOT bail out of the execution as a whole.
continue;
}
// We build the contracts. If building the contracts for the metadata file fails then we
// have no other option but to keep note of this error and move on to the next solc mode
// and NOT just bail out of the execution as a whole.
@@ -907,6 +708,7 @@ where
// For cases if one of the inputs fail then we move on to the next case and we do NOT
// bail out of the whole thing.
'case_loop: for (case_idx, case) in self.metadata.cases.iter().enumerate() {
let tracing_span = tracing::info_span!(
"Handling case",
@@ -915,7 +717,7 @@ where
);
let _guard = tracing_span.enter();
let case_idx = CaseIdx::new(case_idx);
let case_idx = CaseIdx::new_from(case_idx);
// For inputs if one of the inputs fail we move on to the next case (we do not move
// on to the next input as it doesn't make sense. It depends on the previous one).
@@ -927,13 +729,8 @@ where
tracing::info_span!("Executing input", contract_name = ?input.instance)
.in_scope(|| {
let (leader_receipt, _, leader_diff) = match leader_state
.handle_input(
self.metadata,
case_idx,
&input,
self.leader_node,
&mode,
) {
.handle_input(self.metadata, case_idx, &input, self.leader_node)
{
Ok(result) => result,
Err(error) => {
tracing::error!(
@@ -962,7 +759,6 @@ where
case_idx,
&input,
self.follower_node,
&mode,
) {
Ok(result) => result,
Err(error) => {
+2 -2
View File
@@ -5,17 +5,17 @@
use revive_dt_compiler::{SolidityCompiler, revive_resolc, solc};
use revive_dt_config::TestingPlatform;
use revive_dt_format::traits::ResolverApi;
use revive_dt_node::{Node, geth, kitchensink::KitchensinkNode};
use revive_dt_node_interaction::EthereumNode;
pub mod common;
pub mod driver;
/// One platform can be tested differentially against another.
///
/// For this we need a blockchain node implementation and a compiler.
pub trait Platform {
type Blockchain: EthereumNode + Node + ResolverApi;
type Blockchain: EthereumNode + Node;
type Compiler: SolidityCompiler;
/// Returns the matching [TestingPlatform] of the [revive_dt_config::Arguments].
+1 -1
View File
@@ -9,7 +9,7 @@ repository.workspace = true
rust-version.workspace = true
[dependencies]
revive-dt-common = { workspace = true }
revive-dt-node-interaction = { workspace = true }
alloy = { workspace = true }
alloy-primitives = { workspace = true }
+2 -3
View File
@@ -1,8 +1,7 @@
use serde::Deserialize;
use revive_dt_common::macros::define_wrapper_type;
use crate::{
define_wrapper_type,
input::{Expected, Input},
mode::Mode,
};
@@ -46,5 +45,5 @@ impl Case {
define_wrapper_type!(
/// A wrapper type for the index of test cases found in metadata file.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CaseIdx(usize);
CaseIdx(usize);
);
+22 -54
View File
@@ -17,25 +17,7 @@ impl Corpus {
/// Try to read and parse the corpus definition file at given `path`.
pub fn try_from_path(path: &Path) -> anyhow::Result<Self> {
let file = File::open(path)?;
let mut corpus: Corpus = serde_json::from_reader(file)?;
// Ensure that the path mentioned in the corpus is relative to the corpus file.
// Canonicalizing also helps make the path in any errors unambiguous.
corpus.path = path
.parent()
.ok_or_else(|| {
anyhow::anyhow!("Corpus path '{}' does not point to a file", path.display())
})?
.canonicalize()
.map_err(|error| {
anyhow::anyhow!(
"Failed to canonicalize path to corpus '{}': {error}",
path.display()
)
})?
.join(corpus.path);
Ok(corpus)
Ok(serde_json::from_reader(file)?)
}
/// Scan the corpus base directory and return all tests found.
@@ -53,47 +35,33 @@ impl Corpus {
///
/// `path` is expected to be a directory.
pub fn collect_metadata(path: &Path, tests: &mut Vec<MetadataFile>) {
if path.is_dir() {
let dir_entry = match std::fs::read_dir(path) {
Ok(dir_entry) => dir_entry,
let dir_entry = match std::fs::read_dir(path) {
Ok(dir_entry) => dir_entry,
Err(error) => {
tracing::error!("failed to read dir '{}': {error}", path.display());
return;
}
};
for entry in dir_entry {
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
tracing::error!("failed to read dir '{}': {error}", path.display());
return;
}
};
for entry in dir_entry {
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
tracing::error!("error reading dir entry: {error}");
continue;
}
};
let path = entry.path();
if path.is_dir() {
collect_metadata(&path, tests);
tracing::error!("error reading dir entry: {error}");
continue;
}
if path.is_file() {
if let Some(metadata) = MetadataFile::try_from_file(&path) {
tests.push(metadata)
}
}
}
} else {
let Some(extension) = path.extension() else {
tracing::error!("Failed to get file extension");
return;
};
if extension.eq_ignore_ascii_case("sol") || extension.eq_ignore_ascii_case("json") {
if let Some(metadata) = MetadataFile::try_from_file(path) {
let path = entry.path();
if path.is_dir() {
collect_metadata(&path, tests);
continue;
}
if path.is_file() {
if let Some(metadata) = MetadataFile::try_from_file(&path) {
tests.push(metadata)
}
} else {
tracing::error!(?extension, "Unsupported file extension");
}
}
}
+295 -661
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -3,6 +3,6 @@
pub mod case;
pub mod corpus;
pub mod input;
pub mod macros;
pub mod metadata;
pub mod mode;
pub mod traits;
@@ -12,9 +12,11 @@
/// pub struct CaseId(usize);
/// ```
///
/// And would also implement a number of methods on this type making it easier to use.
/// And would also implement a number of methods on this type making it easier
/// to use.
///
/// These wrapper types become very useful as they make the code a lot easier to read.
/// These wrapper types become very useful as they make the code a lot easier
/// to read.
///
/// Take the following as an example:
///
@@ -24,31 +26,33 @@
/// }
/// ```
///
/// In the above code it's hard to understand what the various types refer to or what to expect them
/// to contain.
/// In the above code it's hard to understand what the various types refer to or
/// what to expect them to contain.
///
/// With these wrapper types we're able to create code that's self-documenting in that the types
/// tell us what the code is referring to. The above code is transformed into
/// With these wrapper types we're able to create code that's self-documenting
/// in that the types tell us what the code is referring to. The above code is
/// transformed into
///
/// ```rust,ignore
/// struct State {
/// contracts: HashMap<CaseId, HashMap<ContractName, ContractByteCode>>
/// }
/// ```
///
/// Note that we follow the same syntax for defining wrapper structs but we do not permit the use of
/// generics.
#[macro_export]
macro_rules! define_wrapper_type {
(
$(#[$meta: meta])*
$vis:vis struct $ident: ident($ty: ty);
$ident: ident($ty: ty) $(;)?
) => {
$(#[$meta])*
$vis struct $ident($ty);
pub struct $ident($ty);
impl $ident {
pub fn new(value: impl Into<$ty>) -> Self {
pub fn new(value: $ty) -> Self {
Self(value)
}
pub fn new_from<T: Into<$ty>>(value: T) -> Self {
Self(value.into())
}
@@ -100,7 +104,3 @@ 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.
pub use define_wrapper_type;
+20 -48
View File
@@ -9,10 +9,9 @@ use std::{
use serde::{Deserialize, Serialize};
use revive_dt_common::{iterators::FilesWithExtensionIterator, macros::define_wrapper_type};
use crate::{
case::Case,
define_wrapper_type,
mode::{Mode, SolcMode},
};
@@ -47,9 +46,9 @@ impl Deref for MetadataFile {
pub struct Metadata {
pub targets: Option<Vec<String>>,
pub cases: Vec<Case>,
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>,
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdentifier>>,
// TODO: Convert into wrapper types for clarity.
pub libraries: Option<BTreeMap<PathBuf, BTreeMap<ContractIdent, ContractInstance>>>,
pub libraries: Option<BTreeMap<String, BTreeMap<String, String>>>,
pub ignore: Option<bool>,
pub modes: Option<Vec<Mode>>,
pub file_path: Option<PathBuf>,
@@ -86,7 +85,7 @@ impl Metadata {
/// Returns the contract sources with canonicalized paths for the files
pub fn contract_sources(
&self,
) -> anyhow::Result<BTreeMap<ContractInstance, ContractPathAndIdent>> {
) -> anyhow::Result<BTreeMap<ContractInstance, ContractPathAndIdentifier>> {
let directory = self.directory()?;
let mut sources = BTreeMap::new();
let Some(contracts) = &self.contracts else {
@@ -95,7 +94,7 @@ impl Metadata {
for (
alias,
ContractPathAndIdent {
ContractPathAndIdentifier {
contract_source_path,
contract_ident,
},
@@ -107,7 +106,7 @@ impl Metadata {
sources.insert(
alias,
ContractPathAndIdent {
ContractPathAndIdentifier {
contract_source_path: absolute_path,
contract_ident,
},
@@ -193,10 +192,10 @@ impl Metadata {
metadata.file_path = Some(path.to_path_buf());
metadata.contracts = Some(
[(
ContractInstance::new("Test"),
ContractPathAndIdent {
ContractInstance::new_from("test"),
ContractPathAndIdentifier {
contract_source_path: path.to_path_buf(),
contract_ident: ContractIdent::new("Test"),
contract_ident: ContractIdent::new_from("Test"),
},
)]
.into(),
@@ -212,51 +211,24 @@ impl Metadata {
}
}
}
/// Returns an iterator over all of the solidity files that needs to be compiled for this
/// [`Metadata`] object
///
/// Note: if the metadata is contained within a solidity file then this is the only file that
/// we wish to compile since this is a self-contained test. Otherwise, if it's a JSON file
/// then we need to compile all of the contracts that are in the directory since imports are
/// allowed in there.
pub fn files_to_compile(&self) -> anyhow::Result<Box<dyn Iterator<Item = PathBuf>>> {
let Some(ref metadata_file_path) = self.file_path else {
anyhow::bail!("The metadata file path is not defined");
};
if metadata_file_path
.extension()
.is_some_and(|extension| extension.eq_ignore_ascii_case("sol"))
{
Ok(Box::new(std::iter::once(metadata_file_path.clone())))
} else {
Ok(Box::new(
FilesWithExtensionIterator::new(self.directory()?).with_allowed_extension("sol"),
))
}
}
}
define_wrapper_type!(
/// Represents a contract instance found a metadata file.
///
/// Typically, this is used as the key to the "contracts" field of metadata files.
#[derive(
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ContractInstance(String);
ContractInstance(String);
);
define_wrapper_type!(
/// Represents a contract identifier found a metadata file.
///
/// A contract identifier is the name of the contract in the source code.
#[derive(
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ContractIdent(String);
ContractIdent(String);
);
/// Represents an identifier used for contracts.
@@ -268,7 +240,7 @@ define_wrapper_type!(
/// ```
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct ContractPathAndIdent {
pub struct ContractPathAndIdentifier {
/// The path of the contract source code relative to the directory containing the metadata file.
pub contract_source_path: PathBuf,
@@ -276,7 +248,7 @@ pub struct ContractPathAndIdent {
pub contract_ident: ContractIdent,
}
impl Display for ContractPathAndIdent {
impl Display for ContractPathAndIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
@@ -287,7 +259,7 @@ impl Display for ContractPathAndIdent {
}
}
impl FromStr for ContractPathAndIdent {
impl FromStr for ContractPathAndIdentifier {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
@@ -323,7 +295,7 @@ impl FromStr for ContractPathAndIdent {
}
}
impl TryFrom<String> for ContractPathAndIdent {
impl TryFrom<String> for ContractPathAndIdentifier {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
@@ -331,8 +303,8 @@ impl TryFrom<String> for ContractPathAndIdent {
}
}
impl From<ContractPathAndIdent> for String {
fn from(value: ContractPathAndIdent) -> Self {
impl From<ContractPathAndIdentifier> for String {
fn from(value: ContractPathAndIdentifier) -> Self {
value.to_string()
}
}
@@ -347,7 +319,7 @@ mod test {
let string = "ERC20/ERC20.sol:ERC20";
// Act
let identifier = ContractPathAndIdent::from_str(string);
let identifier = ContractPathAndIdentifier::from_str(string);
// Assert
let identifier = identifier.expect("Failed to parse");
-10
View File
@@ -1,4 +1,3 @@
use revive_dt_common::types::VersionOrRequirement;
use semver::Version;
use serde::de::Deserializer;
use serde::{Deserialize, Serialize};
@@ -79,15 +78,6 @@ impl SolcMode {
None
}
/// Resolves the [`SolcMode`]'s solidity version requirement into a [`VersionOrRequirement`] if
/// the requirement is present on the object. Otherwise, the passed default version is used.
pub fn compiler_version_to_use(&self, default: Version) -> VersionOrRequirement {
match self.solc_version {
Some(ref requirement) => requirement.clone().into(),
None => default.into(),
}
}
}
impl<'de> Deserialize<'de> for Mode {
-30
View File
@@ -1,30 +0,0 @@
use alloy::eips::BlockNumberOrTag;
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
use anyhow::Result;
/// A trait of the interface are required to implement to be used by the resolution logic that this
/// crate implements to go from string calldata and into the bytes calldata.
pub trait ResolverApi {
/// Returns the ID of the chain that the node is on.
fn chain_id(&self) -> Result<ChainId>;
// TODO: This is currently a u128 due to Kitchensink needing more than 64 bits for its gas limit
// when we implement the changes to the gas we need to adjust this to be a u64.
/// Returns the gas limit of the specified block.
fn block_gas_limit(&self, number: BlockNumberOrTag) -> Result<u128>;
/// Returns the coinbase of the specified block.
fn block_coinbase(&self, number: BlockNumberOrTag) -> Result<Address>;
/// Returns the difficulty of the specified block.
fn block_difficulty(&self, number: BlockNumberOrTag) -> Result<U256>;
/// Returns the hash of the specified block.
fn block_hash(&self, number: BlockNumberOrTag) -> Result<BlockHash>;
/// Returns the timestamp of the specified block,
fn block_timestamp(&self, number: BlockNumberOrTag) -> Result<BlockTimestamp>;
/// Returns the number of the last block.
fn last_block_number(&self) -> Result<BlockNumber>;
}
+4
View File
@@ -11,3 +11,7 @@ rust-version.workspace = true
[dependencies]
alloy = { workspace = true }
anyhow = { workspace = true }
futures = { workspace = true }
tracing = { workspace = true }
once_cell = { workspace = true }
tokio = { workspace = true }
@@ -23,7 +23,7 @@ use tracing::Instrument;
/// executor to drive an async computation:
///
/// ```rust
/// use revive_dt_common::concepts::*;
/// use revive_dt_node_interaction::*;
///
/// fn blocking_function() {
/// let result = BlockingExecutor::execute(async move {
@@ -134,17 +134,22 @@ impl BlockingExecutor {
}
};
let result = match result {
Ok(result) => result,
match result.map(|result| {
*result
.downcast::<R>()
.expect("Type mismatch in the downcast")
}) {
Ok(result) => Ok(result),
Err(error) => {
tracing::error!(?error, "An error occurred when running the async task");
anyhow::bail!("An error occurred when running the async task: {error:?}")
tracing::error!(
?error,
"Failed to downcast the returned result into the expected type"
);
anyhow::bail!(
"Failed to downcast the returned result into the expected type: {error:?}"
)
}
};
Ok(*result
.downcast::<R>()
.expect("An error occurred when downcasting into R. This is a bug"))
}
}
}
/// Represents the state of the async runtime. This runtime is designed to be a singleton runtime
@@ -203,9 +208,7 @@ mod test {
fn panics_in_futures_are_caught() {
// Act
let result = BlockingExecutor::execute(async move {
panic!(
"If this panic causes, well, a panic, then this is an issue. If it's caught then all good!"
);
panic!("This is a panic!");
0xFFu8
});
+28
View File
@@ -1,9 +1,14 @@
//! This crate implements all node interactions.
use alloy::eips::BlockNumberOrTag;
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
use alloy::rpc::types::{TransactionReceipt, TransactionRequest};
use anyhow::Result;
mod blocking_executor;
pub use blocking_executor::*;
/// An interface for all interactions with Ethereum compatible nodes.
pub trait EthereumNode {
/// Execute the [TransactionRequest] and return a [TransactionReceipt].
@@ -18,4 +23,27 @@ pub trait EthereumNode {
/// Returns the state diff of the transaction hash in the [TransactionReceipt].
fn state_diff(&self, receipt: &TransactionReceipt) -> Result<DiffMode>;
/// Returns the ID of the chain that the node is on.
fn chain_id(&self) -> Result<ChainId>;
// TODO: This is currently a u128 due to Kitchensink needing more than 64 bits for its gas limit
// when we implement the changes to the gas we need to adjust this to be a u64.
/// Returns the gas limit of the specified block.
fn block_gas_limit(&self, number: BlockNumberOrTag) -> Result<u128>;
/// Returns the coinbase of the specified block.
fn block_coinbase(&self, number: BlockNumberOrTag) -> Result<Address>;
/// Returns the difficulty of the specified block.
fn block_difficulty(&self, number: BlockNumberOrTag) -> Result<U256>;
/// Returns the hash of the specified block.
fn block_hash(&self, number: BlockNumberOrTag) -> Result<BlockHash>;
/// Returns the timestamp of the specified block,
fn block_timestamp(&self, number: BlockNumberOrTag) -> Result<BlockTimestamp>;
/// Returns the number of the last block.
fn last_block_number(&self) -> Result<BlockNumber>;
}
+1 -3
View File
@@ -14,10 +14,8 @@ alloy = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true }
revive-dt-common = { workspace = true }
revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true }
revive-dt-node-interaction = { workspace = true }
revive-dt-config = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
+41 -42
View File
@@ -25,10 +25,8 @@ use alloy::{
},
signers::local::PrivateKeySigner,
};
use revive_dt_common::concepts::BlockingExecutor;
use revive_dt_config::Arguments;
use revive_dt_format::traits::ResolverApi;
use revive_dt_node_interaction::EthereumNode;
use revive_dt_node_interaction::{BlockingExecutor, EthereumNode};
use tracing::Level;
use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
@@ -76,8 +74,6 @@ impl Instance {
const GETH_STDOUT_LOG_FILE_NAME: &str = "node_stdout.log";
const GETH_STDERR_LOG_FILE_NAME: &str = "node_stderr.log";
const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress";
/// Create the node directory and call `geth init` to configure the genesis.
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn init(&mut self, genesis: String) -> anyhow::Result<&mut Self> {
@@ -152,10 +148,6 @@ impl Instance {
.arg("--nodiscover")
.arg("--maxpeers")
.arg("0")
.arg("--txlookuplimit")
.arg("0")
.arg("--cache.blocklogs")
.arg("512")
.stderr(stderr_logs_file.try_clone()?)
.stdout(stdout_logs_file.try_clone()?)
.spawn()?
@@ -275,48 +267,57 @@ impl EthereumNode for Instance {
// 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.
// At the moment we do not allow for the 60 seconds to be modified and we take it as
// being an implementation detail that's invisible to anything outside of this module.
//
// We allow a total of 60 retries for getting the receipt with one second between each
// retry and the next which means that we allow for a total of 60 seconds of waiting
// before we consider that we're unable to get the transaction receipt.
let mut retries = 0;
let mut total_wait_duration = Duration::from_secs(0);
let max_allowed_wait_duration = Duration::from_secs(5 * 60);
loop {
if total_wait_duration >= max_allowed_wait_duration {
tracing::error!(
?total_wait_duration,
?max_allowed_wait_duration,
retry_count = retries,
"Failed to get receipt after polling for it"
);
anyhow::bail!(
"Polled for receipt for {total_wait_duration:?} but failed to get it"
);
}
match provider.get_transaction_receipt(*transaction_hash).await {
Ok(Some(receipt)) => {
tracing::info!(?total_wait_duration, "Found receipt");
tracing::info!("Obtained the transaction receipt");
break Ok(receipt);
}
Ok(None) => {}
Ok(None) => {
if retries == 60 {
tracing::error!(
"Polled for transaction receipt for 60 seconds but failed to get it"
);
break Err(anyhow::anyhow!("Failed to get the transaction receipt"));
} else {
tracing::trace!(
retries,
"Sleeping for 1 second and trying to get the receipt again"
);
retries += 1;
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
continue;
}
}
Err(error) => {
let error_string = error.to_string();
if !error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
if error_string.contains("transaction indexing is in progress") {
if retries == 60 {
tracing::error!(
"Polled for transaction receipt for 60 seconds but failed to get it"
);
break Err(error.into());
} else {
tracing::trace!(
retries,
"Sleeping for 1 second and trying to get the receipt again"
);
retries += 1;
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
continue;
}
} else {
break Err(error.into());
}
}
};
let next_wait_duration = Duration::from_secs(2u64.pow(retries))
.min(max_allowed_wait_duration - total_wait_duration);
total_wait_duration += next_wait_duration;
retries += 1;
tokio::time::sleep(next_wait_duration).await;
}
}
})?
}
@@ -352,9 +353,7 @@ impl EthereumNode for Instance {
_ => anyhow::bail!("expected a diff mode trace"),
}
}
}
impl ResolverApi for Instance {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
let provider = self.provider();
+1 -5
View File
@@ -30,16 +30,14 @@ use alloy::{
},
signers::local::PrivateKeySigner,
};
use revive_dt_format::traits::ResolverApi;
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use sp_core::crypto::Ss58Codec;
use sp_runtime::AccountId32;
use tracing::Level;
use revive_dt_common::concepts::BlockingExecutor;
use revive_dt_config::Arguments;
use revive_dt_node_interaction::EthereumNode;
use revive_dt_node_interaction::{BlockingExecutor, EthereumNode};
use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
@@ -426,9 +424,7 @@ impl EthereumNode for KitchensinkNode {
_ => anyhow::bail!("expected a diff mode trace"),
}
}
}
impl ResolverApi for KitchensinkNode {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
let provider = self.provider();
+1 -1
View File
@@ -10,9 +10,9 @@ rust-version.workspace = true
[dependencies]
revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true }
revive-dt-compiler = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
revive-solc-json-interface = { workspace = true }
+21 -8
View File
@@ -1,6 +1,5 @@
//! The report analyzer enriches the raw report data.
use revive_dt_compiler::CompilerOutput;
use serde::{Deserialize, Serialize};
use crate::reporter::CompilationTask;
@@ -14,27 +13,41 @@ pub struct CompilerStatistics {
pub mean_code_size: usize,
/// The mean size of the optimized YUL IR.
pub mean_yul_size: usize,
/// Is a proxy because the YUL also contains a lot of comments.
/// Is a proxy because the YUL also containes a lot of comments.
pub yul_to_bytecode_size_ratio: f32,
}
impl CompilerStatistics {
/// Cumulatively update the statistics with the next compiler task.
pub fn sample(&mut self, compilation_task: &CompilationTask) {
let Some(CompilerOutput { contracts }) = &compilation_task.json_output else {
let Some(output) = &compilation_task.json_output else {
return;
};
let Some(contracts) = &output.contracts else {
return;
};
for (_solidity, contracts) in contracts.iter() {
for (_name, (bytecode, _)) in contracts.iter() {
for (_name, contract) in contracts.iter() {
let Some(evm) = &contract.evm else {
continue;
};
let Some(deploy_code) = &evm.deployed_bytecode else {
continue;
};
// The EVM bytecode can be unlinked and thus is not necessarily a decodable hex
// string; for our statistics this is a good enough approximation.
let bytecode_size = bytecode.len() / 2;
let bytecode_size = deploy_code.object.len() / 2;
// TODO: for the time being we set the yul_size to be zero. We need to change this
// when we overhaul the reporting.
let yul_size = contract
.ir_optimized
.as_ref()
.expect("if the contract has a deploy code it should also have the opimized IR")
.len();
self.update_sizes(bytecode_size, 0);
self.update_sizes(bytecode_size, yul_size);
}
}
}
+12 -4
View File
@@ -12,11 +12,11 @@ use std::{
};
use anyhow::Context;
use revive_dt_compiler::{CompilerInput, CompilerOutput};
use serde::{Deserialize, Serialize};
use revive_dt_config::{Arguments, TestingPlatform};
use revive_dt_format::{corpus::Corpus, mode::SolcMode};
use revive_solc_json_interface::{SolcStandardJsonInput, SolcStandardJsonOutput};
use crate::analyzer::CompilerStatistics;
@@ -44,9 +44,9 @@ pub struct Report {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompilationTask {
/// The observed compiler input.
pub json_input: CompilerInput,
pub json_input: SolcStandardJsonInput,
/// The observed compiler output.
pub json_output: Option<CompilerOutput>,
pub json_output: Option<SolcStandardJsonOutput>,
/// The observed compiler mode.
pub mode: SolcMode,
/// The observed compiler version.
@@ -152,7 +152,15 @@ impl Report {
for (platform, results) in self.compiler_results.iter() {
for result in results {
// ignore if there were no errors
if result.compilation_task.error.is_none() {
if result.compilation_task.error.is_none()
&& result
.compilation_task
.json_output
.as_ref()
.and_then(|output| output.errors.as_ref())
.map(|errors| errors.is_empty())
.unwrap_or(true)
{
continue;
}
-2
View File
@@ -9,8 +9,6 @@ repository.workspace = true
rust-version.workspace = true
[dependencies]
revive-dt-common = { workspace = true }
anyhow = { workspace = true }
hex = { workspace = true }
tracing = { workspace = true }
+13 -38
View File
@@ -5,8 +5,6 @@ use std::{
sync::{LazyLock, Mutex},
};
use revive_dt_common::types::VersionOrRequirement;
use semver::Version;
use sha2::{Digest, Sha256};
@@ -54,50 +52,27 @@ impl GHDownloader {
pub const WINDOWS_NAME: &str = "solc-windows.exe";
pub const WASM_NAME: &str = "soljson.js";
fn new(
version: impl Into<VersionOrRequirement>,
target: &'static str,
list: &'static str,
) -> anyhow::Result<Self> {
let version_or_requirement = version.into();
match version_or_requirement {
VersionOrRequirement::Version(version) => Ok(Self {
version,
target,
list,
}),
VersionOrRequirement::Requirement(requirement) => {
let Some(version) = List::download(list)?
.builds
.into_iter()
.map(|build| build.version)
.filter(|version| requirement.matches(version))
.max()
else {
anyhow::bail!("Failed to find a version that satisfies {requirement:?}");
};
Ok(Self {
version,
target,
list,
})
}
fn new(version: Version, target: &'static str, list: &'static str) -> Self {
Self {
version,
target,
list,
}
}
pub fn linux(version: impl Into<VersionOrRequirement>) -> anyhow::Result<Self> {
pub fn linux(version: Version) -> Self {
Self::new(version, Self::LINUX_NAME, List::LINUX_URL)
}
pub fn macosx(version: impl Into<VersionOrRequirement>) -> anyhow::Result<Self> {
pub fn macosx(version: Version) -> Self {
Self::new(version, Self::MACOSX_NAME, List::MACOSX_URL)
}
pub fn windows(version: impl Into<VersionOrRequirement>) -> anyhow::Result<Self> {
pub fn windows(version: Version) -> Self {
Self::new(version, Self::WINDOWS_NAME, List::WINDOWS_URL)
}
pub fn wasm(version: impl Into<VersionOrRequirement>) -> anyhow::Result<Self> {
pub fn wasm(version: Version) -> Self {
Self::new(version, Self::WASM_NAME, List::WASM_URL)
}
@@ -136,24 +111,24 @@ mod tests {
#[test]
fn try_get_windows() {
let version = List::download(List::WINDOWS_URL).unwrap().latest_release;
GHDownloader::windows(version).unwrap().download().unwrap();
GHDownloader::windows(version).download().unwrap();
}
#[test]
fn try_get_macosx() {
let version = List::download(List::MACOSX_URL).unwrap().latest_release;
GHDownloader::macosx(version).unwrap().download().unwrap();
GHDownloader::macosx(version).download().unwrap();
}
#[test]
fn try_get_linux() {
let version = List::download(List::LINUX_URL).unwrap().latest_release;
GHDownloader::linux(version).unwrap().download().unwrap();
GHDownloader::linux(version).download().unwrap();
}
#[test]
fn try_get_wasm() {
let version = List::download(List::WASM_URL).unwrap().latest_release;
GHDownloader::wasm(version).unwrap().download().unwrap();
GHDownloader::wasm(version).download().unwrap();
}
}
+3 -4
View File
@@ -7,8 +7,7 @@ use std::path::{Path, PathBuf};
use cache::get_or_download;
use download::GHDownloader;
use revive_dt_common::types::VersionOrRequirement;
use semver::Version;
pub mod cache;
pub mod download;
@@ -21,7 +20,7 @@ pub mod list;
/// and not download it again.
pub fn download_solc(
cache_directory: &Path,
version: impl Into<VersionOrRequirement>,
version: Version,
wasm: bool,
) -> anyhow::Result<PathBuf> {
let downloader = if wasm {
@@ -34,7 +33,7 @@ pub fn download_solc(
GHDownloader::windows(version)
} else {
unimplemented!()
}?;
};
get_or_download(cache_directory, &downloader)
}