Compare commits

...

30 Commits

Author SHA1 Message Date
Omar Abdulla 91773dd281 Correct the key and value encoding for the cache 2025-08-16 18:18:43 +03:00
Omar Abdulla c34f5a3d95 Implement a persistent compilation cache 2025-08-16 17:11:04 +03:00
Omar Abdulla 19075f76e0 Merge branch 'main' into feature/cached-compiler-artifacts 2025-08-16 14:57:45 +03:00
Omar Abdulla 0038634695 WIP compilation cache 2025-08-16 14:55:02 +03:00
James Wilson 09d56f5177 Redo how we parse and use modes (#125)
* WIP redo how we parse and use modes

* test expanding, too

* WIP integrate new Mode/ParsedMode into rest of code

* First pass integrated new mode bits

* fmt

* clippy

* Remove mode we no longer support from test metadata

* Address nits

* Add ability for compiler to opt out if it can't work with some Mode/version

* Elide viaIR input if compiler does not support it

* Improve test output a little; string modes and list ignored tests

* Move Mode to common crate

* constants.mod, and Display for CaseIdx to use it

* fmt

* Rename ModePipeline::E/Y

* Re-arrange Mode things; ParsedMode in format and Mode etc in common

* Move compile check to prepare_tests

* Remove now-unused deps

* clippy nits

* Update fallback tx weights to avoid out of gas errors

* Update kitchensink weights too and fmt

* Bump default geth timeout to 10s

* 30s timeout

* Improve geth stdout logging on failure

* fix line logging

* remove --networkid and arg, back to 5s timeout for geth
2025-08-16 11:38:17 +00:00
Omar a59e287fa1 Add a cached fs abstraction (#141) 2025-08-14 15:21:05 +00:00
Omar f2045db0e9 Add compiler directives to metadata (#139) 2025-08-14 07:38:56 +00:00
Omar 5a11f44673 Misc features/improvements (#138)
* Implement various needed features and improvements

* Reorder the metadata struct

* Format comments
2025-08-13 13:50:06 +00:00
James Wilson 46aea0890d Split reporter and case runner, use channels to pass test reports (#137)
* Use channels to send data to reporting thread and avoid hangs / mutex / duration. Limit max concurrent tasks to avoid too many open files

* More appropriate name for dirver/reporter task fns

* Back to parallelise individual cases, report individual cases, address grumbles

* newline before 'Failures' title in report
2025-08-13 13:10:26 +00:00
Omar 9b40c9b9e3 Add an EVM version filter (#136)
* Add an EVM version filter

* Update naming
2025-08-12 10:19:59 +00:00
Omar f67a9bf643 Refactor/ignore null values (#135)
* Skip serialization of null values

* Add support for comments in various steps
2025-08-12 08:55:21 +00:00
Omar 67d767ffde Implement storage empty assertion (#134) 2025-08-11 13:17:19 +00:00
Omar f7fbe094ec Balance assertions (#133)
* Make metadata serializable

* Refactor tests to use steps

* Add a balance assertion test step

* Test balance deserialization

* Box the test steps

* Permit size difference in step output
2025-08-11 12:11:16 +00:00
Omar 90b2dd4cfe Make metadata serializable (#132) 2025-08-10 21:57:41 +00:00
Omar 64d63ef999 Remove the provider cache (#121)
* Remove the provider cache

* Add timing information to the CLI report
2025-08-07 03:55:24 +00:00
Omar 757bfbe116 Add more resolvable variables (#120)
* Allow resolution of base fee

* Fix block difficulty resolution

* Allow for the resolution of gas price
2025-08-06 15:17:36 +00:00
Omar 8619e7feb0 Fix the transaction tracing issues (#118)
* Set the gc mode to archive in geth

* Add a maximum to the exponential backoff wait duration

* Edit the formatting of the CLI case reporter
2025-08-06 12:25:39 +00:00
Omar edba49b301 Use SolidityLang for solc downloads (#117) 2025-08-06 10:35:05 +00:00
Omar 9980926d40 Add a case ignore flag (#114)
* Added a resolver tied to a specific block

* Increase the number of private keys

* Increase kitchensink wait time to 60 seconds

* Add a case ignore flag
2025-08-04 16:40:53 +00:00
Omar ff993d44a5 Added a resolver tied to a specific block (#111)
* Added a resolver tied to a specific block

* Increase the number of private keys

* Increase kitchensink wait time to 60 seconds
2025-08-04 12:45:47 +00:00
Omar 8cbb1a9f77 Added basic console reporting (#110)
* Added basic console reporting

* Add some waiting period to the printing task

* Print to the stderr and print logs to stdout
2025-08-04 06:05:49 +00:00
Omar 56c2fe8c0c Parallelize Cases (#109)
* Parallelize over cases

* Rename the state and driver

* Parallelize execution

* Update the default config of the tool

* Make codebase async

* Fix machete

* Fix tests & clear node directories before startup

* Cleanup the cleanup logic

* Rename geth node
2025-08-01 11:00:08 +00:00
Omar 330a773a1c Add variables support (#96) 2025-07-30 08:41:03 +00:00
Omar f51693cb9f Support multiple compiler versions (#92)
* Allow for downloader to use version requirements.

We will soon add support for the compiler version requirement from the
metadata files to be honored. The compiler version is specified in the
solc modes section of the file and its specified as a `VersionReq` and
not as a version.

Therefore, we need to have the ability to honor this version requirement
and find the best version that satisfies the requirement.

* Request `VersionOrRequirement` in compiler interface

* Honor the compiler version requirement in metadata

This commit honors the compiler version requirement listed in the solc
modes of the metadata file. If this version requirement is provided then
it overrides what was passed in the CLI. Otherwise, the CLI version will
be used.

* Make compiler IO completely generic.

Before this commit, the types that were used for the compiler input and
output were the resolc compiler types which was a leaky abstraction as
we have traits to abstract the compilers away but we expose their
internal types out to other crates.

This commit did the following:
1. Made the compiler IO types fully generic so that all of the logic for
   constructing the map of compiled contracts is all done by the
   compiler implementation and not by the consuming code.
2. Changed the input types used for Solc to be the forge standard JSON
   types for Solc instead of resolc.

* Fix machete

* Add resolc to CI

* Add resolc to CI

* Add resolc to CI

* Add resolc to CI
2025-07-30 04:56:23 +00:00
James Wilson 4db7009640 Ensure path in corpus is relative to corpus file (#85) 2025-07-29 13:12:16 +00:00
Omar 5a36e242ec Allow for files in corpus definitions (#87)
* Allow for files to be specified in the corpus file

* Attempt to improve the geth tx indexing issue.

We're facing an issue where Geth transaction indexing can sometimes stall
on some of the nodes we're running. The logs show that for all transactions
we always need 1 second of waiting time. However, during certain runs we
sometimes run into an issue with some of the nodes where it seems like
their transaction indexer fails (either at the start or after some amount
of time) which leads us to never get the receipts back from these specific
nodes.

This is not a load issue as it appears like all of the other nodes handle
it just fine. However, it looks like once a node gets into this state it
can not get out of it and its bricked for the entire run.

This commit adds some more command line arguments to the geth command in
hopes of improving this issue.
2025-07-29 13:02:53 +00:00
James Wilson 33329632b5 Increase geth instantiate timeout from 2s to 5s (#86) 2025-07-29 10:34:31 +00:00
Omar 429f2e92a2 Fix contract discovery for simple tests (#83) 2025-07-28 07:05:53 +00:00
Omar 65f41f2038 Correct the type of address in matterlabs events (#82) 2025-07-28 05:01:52 +00:00
Omar 3ed8a1ca1c Support compiler-version aware exceptions (#81) 2025-07-25 14:23:17 +00:00
52 changed files with 4937 additions and 2141 deletions
+16
View File
@@ -99,9 +99,12 @@ 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
@@ -122,12 +125,22 @@ 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
@@ -143,5 +156,8 @@ 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
+719 -20
View File
File diff suppressed because it is too large Load Diff
+12 -4
View File
@@ -8,7 +8,7 @@ authors = ["Parity Technologies <admin@parity.io>"]
license = "MIT/Apache-2.0"
edition = "2024"
repository = "https://github.com/paritytech/revive-differential-testing.git"
rust-version = "1.85.0"
rust-version = "1.87.0"
[workspace.dependencies]
revive-dt-common = { version = "0.1.0", path = "crates/common" }
@@ -25,25 +25,33 @@ revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" }
alloy-primitives = "1.2.1"
alloy-sol-types = "1.2.1"
anyhow = "1.0"
bson = { version = "2.15.0" }
cacache = { version = "13.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"] }
regex = "1"
moka = "0.12.10"
reqwest = { version = "0.12.15", features = ["json"] }
once_cell = "1.21"
rayon = { version = "1.10" }
semver = { version = "1.0", features = ["serde"] }
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = { version = "1.0", default-features = false, features = [
"arbitrary_precision",
"std",
"unbounded_depth",
] }
sha2 = { version = "0.10.9" }
sp-core = "36.1.0"
sp-runtime = "41.1.0"
temp-dir = { version = "0.1.16" }
tempfile = "3.3"
tokio = { version = "1", default-features = false, features = [
thiserror = "2"
tokio = { version = "1.47.0", default-features = false, features = [
"rt-multi-thread",
"process",
"rt",
] }
uuid = { version = "1.8", features = ["v4"] }
tracing = "0.1.41"
+13 -2
View File
@@ -1,13 +1,24 @@
{
"modes": [
"Y >=0.8.9",
"E",
"I"
"E"
],
"cases": [
{
"name": "first",
"inputs": [
{
"address": "0xdeadbeef00000000000000000000000000000042",
"expected_balance": "1233"
},
{
"address": "0xdeadbeef00000000000000000000000000000042",
"is_storage_empty": true
},
{
"address": "0xdeadbeef00000000000000000000000000000042",
"is_storage_empty": false
},
{
"instance": "WBTC_1",
"method": "#deployer",
+4 -3
View File
@@ -10,7 +10,8 @@ rust-version.workspace = true
[dependencies]
anyhow = { workspace = true }
futures = { workspace = true }
tracing = { workspace = true }
moka = { workspace = true, features = ["sync"] }
once_cell = { workspace = true }
tokio = { workspace = true }
semver = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true, default-features = false, features = ["time"] }
+49
View File
@@ -0,0 +1,49 @@
//! 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 moka::sync::Cache;
use once_cell::sync::Lazy;
pub fn read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
static READ_CACHE: Lazy<Cache<PathBuf, Vec<u8>>> = Lazy::new(|| Cache::new(10_000));
let path = path.as_ref().canonicalize()?;
match READ_CACHE.get(path.as_path()) {
Some(content) => Ok(content),
None => {
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",
)
})
}
pub fn read_dir(path: impl AsRef<Path>) -> Result<Box<dyn Iterator<Item = Result<PathBuf>>>> {
static READ_DIR_CACHE: Lazy<Cache<PathBuf, Vec<PathBuf>>> = Lazy::new(|| Cache::new(10_000));
let path = path.as_ref().canonicalize()?;
match READ_DIR_CACHE.get(path.as_path()) {
Some(entries) => Ok(Box::new(entries.into_iter().map(Ok)) as Box<_>),
None => {
let entries = fs::read_dir(path.as_path())?
.flat_map(|maybe_entry| maybe_entry.map(|entry| entry.path()))
.collect();
READ_DIR_CACHE.insert(path.clone(), entries);
Ok(read_dir(path).unwrap())
}
}
}
@@ -1,225 +0,0 @@
//! The alloy crate __requires__ a tokio runtime.
//! We contain any async rust right here.
use std::{any::Any, panic::AssertUnwindSafe, pin::Pin, thread};
use futures::FutureExt;
use once_cell::sync::Lazy;
use tokio::{
runtime::Builder,
sync::{mpsc::UnboundedSender, oneshot},
};
use tracing::Instrument;
/// A blocking async executor.
///
/// This struct exposes the abstraction of a blocking async executor. It is a global and static
/// executor which means that it doesn't require for new instances of it to be created, it's a
/// singleton and can be accessed by any thread that wants to perform some async computation on the
/// blocking executor thread.
///
/// The API of the blocking executor is created in a way so that it's very natural, simple to use,
/// and unbounded to specific tasks or return types. The following is an example of using this
/// executor to drive an async computation:
///
/// ```rust
/// use revive_dt_common::concepts::*;
///
/// fn blocking_function() {
/// let result = BlockingExecutor::execute(async move {
/// tokio::time::sleep(std::time::Duration::from_secs(1)).await;
/// 0xFFu8
/// })
/// .expect("Computation failed");
///
/// assert_eq!(result, 0xFF);
/// }
/// ```
///
/// Users get to pass in their async tasks without needing to worry about putting them in a [`Box`],
/// [`Pin`], needing to perform down-casting, or the internal channel mechanism used by the runtime.
/// To the user, it just looks like a function that converts some async code into sync code.
///
/// This struct also handled panics that occur in the passed futures and converts them into errors
/// that can be handled by the user. This is done to allow the executor to be robust.
///
/// Internally, the executor communicates with the tokio runtime thread through channels which carry
/// the [`TaskMessage`] and the results of the execution.
pub struct BlockingExecutor;
impl BlockingExecutor {
pub fn execute<R>(future: impl Future<Output = R> + Send + 'static) -> Result<R, anyhow::Error>
where
R: Send + 'static,
{
// Note: The blocking executor is a singleton and therefore we store its state in a static
// so that it's assigned only once. Additionally, when we set the state of the executor we
// spawn the thread where the async runtime runs.
static STATE: Lazy<ExecutorState> = Lazy::new(|| {
tracing::trace!("Initializing the BlockingExecutor state");
// All communication with the tokio runtime thread happens over mspc channels where the
// producers here are the threads that want to run async tasks and the consumer here is
// the tokio runtime thread.
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<TaskMessage>();
thread::spawn(move || {
tracing::info!(
thread_id = ?std::thread::current().id(),
"Starting async runtime thread"
);
let runtime = Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create the async runtime");
runtime.block_on(async move {
while let Some(TaskMessage {
future: task,
response_tx: response_channel,
}) = rx.recv().await
{
tracing::trace!("Received a new future to execute");
tokio::spawn(async move {
// One of the things that the blocking executor does is that it allows
// us to catch panics if they occur. By wrapping the given future in an
// AssertUnwindSafe::catch_unwind we are able to catch all panic unwinds
// in the given future and convert them into errors.
let task = AssertUnwindSafe(task).catch_unwind();
let result = task.await;
let _ = response_channel.send(result);
});
}
})
});
ExecutorState { tx }
});
// We need to perform blocking synchronous communication between the current thread and the
// tokio runtime thread with the result of the async computation and the oneshot channels
// from tokio allows us to do that. The sender side of the channel will be given to the
// tokio runtime thread to send the result when the computation is completed and the receive
// side of the channel will be kept with this thread to await for the response of the async
// task to come back.
let (response_tx, response_rx) =
oneshot::channel::<Result<Box<dyn Any + Send>, Box<dyn Any + Send>>>();
// The tokio runtime thread expects a Future<Output = Box<dyn Any + Send>> + Send to be
// sent to it to execute. However, this function has a typed Future<Output = R> + Send and
// therefore we need to change the type of the future to fit what the runtime thread expects
// in the task message. In doing this conversion, we lose some of the type information since
// we're converting R => dyn Any. However, we will perform down-casting on the result to
// convert it back into R.
let future = Box::pin(
async move { Box::new(future.await) as Box<dyn Any + Send> }.in_current_span(),
);
let task = TaskMessage::new(future, response_tx);
if let Err(error) = STATE.tx.send(task) {
tracing::error!(?error, "Failed to send the task to the blocking executor");
anyhow::bail!("Failed to send the task to the blocking executor: {error:?}")
}
let result = match response_rx.blocking_recv() {
Ok(result) => result,
Err(error) => {
tracing::error!(
?error,
"Failed to get the response from the blocking executor"
);
anyhow::bail!("Failed to get the response from the blocking executor: {error:?}")
}
};
let result = match result {
Ok(result) => 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:?}")
}
};
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
/// which means that in the current running program there's just a single thread that has an async
/// runtime.
struct ExecutorState {
/// The sending side of the task messages channel. This is used by all of the other threads to
/// communicate with the async runtime thread.
tx: UnboundedSender<TaskMessage>,
}
/// Represents a message that contains an asynchronous task that's to be executed by the runtime
/// as well as a way for the runtime to report back on the result of the execution.
struct TaskMessage {
/// The task that's being requested to run. This is a future that returns an object that does
/// implement [`Any`] and [`Send`] to allow it to be sent between the requesting thread and the
/// async thread.
future: Pin<Box<dyn Future<Output = Box<dyn Any + Send>> + Send>>,
/// A one shot sender channel where the sender of the task is expecting to hear back on the
/// result of the task.
response_tx: oneshot::Sender<Result<Box<dyn Any + Send>, Box<dyn Any + Send>>>,
}
impl TaskMessage {
pub fn new(
future: Pin<Box<dyn Future<Output = Box<dyn Any + Send>> + Send>>,
response_tx: oneshot::Sender<Result<Box<dyn Any + Send>, Box<dyn Any + Send>>>,
) -> Self {
Self {
future,
response_tx,
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn simple_future_works() {
// Act
let result = BlockingExecutor::execute(async move {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
0xFFu8
})
.unwrap();
// Assert
assert_eq!(result, 0xFFu8);
}
#[test]
#[allow(unreachable_code, clippy::unreachable)]
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!"
);
0xFFu8
});
// Assert
assert!(result.is_err());
// Act
let result = BlockingExecutor::execute(async move {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
0xFFu8
})
.unwrap();
// Assert
assert_eq!(result, 0xFFu8)
}
}
-3
View File
@@ -1,3 +0,0 @@
mod blocking_executor;
pub use blocking_executor::*;
+22
View File
@@ -0,0 +1,22 @@
use std::{
fs::{read_dir, remove_dir_all, remove_file},
path::Path,
};
use anyhow::Result;
/// This method clears the passed directory of all of the files and directories contained within
/// without deleting the directory.
pub fn clear_directory(path: impl AsRef<Path>) -> Result<()> {
for entry in read_dir(path.as_ref())? {
let entry = entry?;
let entry_path = entry.path();
if entry_path.is_file() {
remove_file(entry_path)?
} else {
remove_dir_all(entry_path)?
}
}
Ok(())
}
+3
View File
@@ -0,0 +1,3 @@
mod clear_dir;
pub use clear_dir::*;
+3
View File
@@ -0,0 +1,3 @@
mod poll;
pub use poll::*;
+69
View File
@@ -0,0 +1,69 @@
use std::ops::ControlFlow;
use std::time::Duration;
use anyhow::{Result, anyhow};
const EXPONENTIAL_BACKOFF_MAX_WAIT_DURATION: Duration = Duration::from_secs(60);
/// A function that polls for a fallible future for some period of time and errors if it fails to
/// get a result after polling.
///
/// Given a future that returns a [`Result<ControlFlow<O, ()>>`], this function calls the future
/// repeatedly (with some wait period) until the future returns a [`ControlFlow::Break`] or until it
/// returns an [`Err`] in which case the function stops polling and returns the error.
///
/// If the future keeps returning [`ControlFlow::Continue`] and fails to return a [`Break`] within
/// the permitted polling duration then this function returns an [`Err`]
///
/// [`Break`]: ControlFlow::Break
/// [`Continue`]: ControlFlow::Continue
pub async fn poll<F, O>(
polling_duration: Duration,
polling_wait_behavior: PollingWaitBehavior,
mut future: impl FnMut() -> F,
) -> Result<O>
where
F: Future<Output = Result<ControlFlow<O, ()>>>,
{
let mut retries = 0;
let mut total_wait_duration = Duration::ZERO;
let max_allowed_wait_duration = polling_duration;
loop {
if total_wait_duration >= max_allowed_wait_duration {
break Err(anyhow!(
"Polling failed after {} retries and a total of {:?} of wait time",
retries,
total_wait_duration
));
}
match future().await? {
ControlFlow::Continue(()) => {
let next_wait_duration = match polling_wait_behavior {
PollingWaitBehavior::Constant(duration) => duration,
PollingWaitBehavior::ExponentialBackoff => {
Duration::from_secs(2u64.pow(retries))
.min(EXPONENTIAL_BACKOFF_MAX_WAIT_DURATION)
}
};
let next_wait_duration =
next_wait_duration.min(max_allowed_wait_duration - total_wait_duration);
total_wait_duration += next_wait_duration;
retries += 1;
tokio::time::sleep(next_wait_duration).await;
}
ControlFlow::Break(output) => {
break Ok(output);
}
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum PollingWaitBehavior {
Constant(Duration),
#[default]
ExponentialBackoff,
}
@@ -1,4 +1,8 @@
use std::{borrow::Cow, collections::HashSet, path::PathBuf};
use std::{
borrow::Cow,
collections::HashSet,
path::{Path, PathBuf},
};
/// An iterator that finds files of a certain extension in the provided directory. You can think of
/// this a glob pattern similar to: `${path}/**/*.md`
@@ -15,14 +19,20 @@ pub struct FilesWithExtensionIterator {
/// this vector then they will be returned when the [`Iterator::next`] method is called. If not
/// then we visit one of the next directories to visit.
files_matching_allowed_extensions: Vec<PathBuf>,
/// This option controls if the the cached file system should be used or not. This could be
/// better for certain cases where the entries in the directories do not change and therefore
/// caching can be used.
use_cached_fs: bool,
}
impl FilesWithExtensionIterator {
pub fn new(root_directory: PathBuf) -> Self {
pub fn new(root_directory: impl AsRef<Path>) -> Self {
Self {
allowed_extensions: Default::default(),
directories_to_search: vec![root_directory],
directories_to_search: vec![root_directory.as_ref().to_path_buf()],
files_matching_allowed_extensions: Default::default(),
use_cached_fs: Default::default(),
}
}
@@ -33,6 +43,11 @@ impl FilesWithExtensionIterator {
self.allowed_extensions.insert(allowed_extension.into());
self
}
pub fn with_use_cached_fs(mut self, use_cached_fs: bool) -> Self {
self.use_cached_fs = use_cached_fs;
self
}
}
impl Iterator for FilesWithExtensionIterator {
@@ -45,16 +60,19 @@ impl Iterator for FilesWithExtensionIterator {
let directory_to_search = self.directories_to_search.pop()?;
// Read all of the entries in the directory. If we failed to read this dir's entires then we
// elect to just ignore it and look in the next directory, we do that by calling the next
// method again on the iterator, which is an intentional decision that we made here instead
// of panicking.
let iterator = if self.use_cached_fs {
let Ok(dir_entries) = crate::cached_fs::read_dir(directory_to_search.as_path()) else {
return self.next();
};
Box::new(dir_entries) as Box<dyn Iterator<Item = std::io::Result<PathBuf>>>
} else {
let Ok(dir_entries) = std::fs::read_dir(directory_to_search) else {
return self.next();
};
Box::new(dir_entries.map(|maybe_entry| maybe_entry.map(|entry| entry.path()))) as Box<_>
};
for entry in dir_entries.flatten() {
let entry_path = entry.path();
for entry_path in iterator.flatten() {
if entry_path.is_dir() {
self.directories_to_search.push(entry_path)
} else if entry_path.is_file()
+4 -1
View File
@@ -1,6 +1,9 @@
//! This crate provides common concepts, functionality, types, macros, and more that other crates in
//! the workspace can benefit from.
pub mod concepts;
pub mod cached_fs;
pub mod fs;
pub mod futures;
pub mod iterators;
pub mod macros;
pub mod types;
+5
View File
@@ -0,0 +1,5 @@
mod mode;
mod version_or_requirement;
pub use mode::*;
pub use version_or_requirement::*;
+167
View File
@@ -0,0 +1,167 @@
use crate::types::VersionOrRequirement;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::str::FromStr;
/// This represents a mode that a given test should be run with, if possible.
///
/// We obtain this by taking a [`ParsedMode`], which may be looser or more strict
/// in its requirements, and then expanding it out into a list of [`Mode`]s.
///
/// Use [`ParsedMode::to_test_modes()`] to do this.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Mode {
pub pipeline: ModePipeline,
pub optimize_setting: ModeOptimizerSetting,
pub version: Option<semver::VersionReq>,
}
impl Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.pipeline.fmt(f)?;
f.write_str(" ")?;
self.optimize_setting.fmt(f)?;
if let Some(version) = &self.version {
f.write_str(" ")?;
version.fmt(f)?;
}
Ok(())
}
}
impl Mode {
/// Return all of the available mode combinations.
pub fn all() -> impl Iterator<Item = Mode> {
ModePipeline::test_cases().flat_map(|pipeline| {
ModeOptimizerSetting::test_cases().map(move |optimize_setting| Mode {
pipeline,
optimize_setting,
version: None,
})
})
}
/// Resolves the [`Mode`]'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.version {
Some(ref requirement) => requirement.clone().into(),
None => default.into(),
}
}
}
/// What do we want the compiler to do?
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum ModePipeline {
/// Compile Solidity code via Yul IR
ViaYulIR,
/// Compile Solidity direct to assembly
ViaEVMAssembly,
}
impl FromStr for ModePipeline {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
// via Yul IR
"Y" => Ok(ModePipeline::ViaYulIR),
// 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'"
)),
}
}
}
impl Display for ModePipeline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModePipeline::ViaYulIR => f.write_str("Y"),
ModePipeline::ViaEVMAssembly => f.write_str("E"),
}
}
}
impl ModePipeline {
/// Should we go via Yul IR?
pub fn via_yul_ir(&self) -> bool {
matches!(self, ModePipeline::ViaYulIR)
}
/// An iterator over the available pipelines that we'd like to test,
/// when an explicit pipeline was not specified.
pub fn test_cases() -> impl Iterator<Item = ModePipeline> + Clone {
[ModePipeline::ViaYulIR, ModePipeline::ViaEVMAssembly].into_iter()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum ModeOptimizerSetting {
/// 0 / -: Don't apply any optimizations
M0,
/// 1: Apply less than default optimizations
M1,
/// 2: Apply the default optimizations
M2,
/// 3 / +: Apply aggressive optimizations
M3,
/// s: Optimize for size
Ms,
/// z: Aggressively optimize for size
Mz,
}
impl FromStr for ModeOptimizerSetting {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"M0" => Ok(ModeOptimizerSetting::M0),
"M1" => Ok(ModeOptimizerSetting::M1),
"M2" => Ok(ModeOptimizerSetting::M2),
"M3" => Ok(ModeOptimizerSetting::M3),
"Ms" => Ok(ModeOptimizerSetting::Ms),
"Mz" => Ok(ModeOptimizerSetting::Mz),
_ => Err(anyhow::anyhow!(
"Unsupported optimizer setting '{s}': expected 'M0', 'M1', 'M2', 'M3', 'Ms' or 'Mz'"
)),
}
}
}
impl Display for ModeOptimizerSetting {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModeOptimizerSetting::M0 => f.write_str("M0"),
ModeOptimizerSetting::M1 => f.write_str("M1"),
ModeOptimizerSetting::M2 => f.write_str("M2"),
ModeOptimizerSetting::M3 => f.write_str("M3"),
ModeOptimizerSetting::Ms => f.write_str("Ms"),
ModeOptimizerSetting::Mz => f.write_str("Mz"),
}
}
}
impl ModeOptimizerSetting {
/// An iterator over the available optimizer settings that we'd like to test,
/// when an explicit optimizer setting was not specified.
pub fn test_cases() -> impl Iterator<Item = ModeOptimizerSetting> + Clone {
[
// No optimizations:
ModeOptimizerSetting::M0,
// Aggressive optimizations:
ModeOptimizerSetting::M3,
]
.into_iter()
}
/// Are any optimizations enabled?
pub fn optimizations_enabled(&self) -> bool {
!matches!(self, ModeOptimizerSetting::M0)
}
}
@@ -0,0 +1,41 @@
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)
}
}
+5
View File
@@ -10,12 +10,17 @@ rust-version.workspace = true
[dependencies]
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 }
tokio = { workspace = true }
+4
View File
@@ -0,0 +1,4 @@
use semver::Version;
/// This is the first version of solc that supports the `--via-ir` flag / "viaIR" input JSON.
pub const SOLC_VERSION_SUPPORTING_VIA_YUL_IR: Version = Version::new(0, 8, 13);
+124 -115
View File
@@ -3,22 +3,26 @@
//! - Polkadot revive resolc compiler
//! - Polkadot revive Wasm compiler
mod constants;
use std::{
fs::read_to_string,
collections::HashMap,
hash::Hash,
path::{Path, PathBuf},
};
use alloy::json_abi::JsonAbi;
use alloy_primitives::Address;
use revive_dt_config::Arguments;
use semver::Version;
use serde::{Deserialize, Serialize};
use revive_common::EVMVersion;
use revive_solc_json_interface::{
SolcStandardJsonInput, SolcStandardJsonInputLanguage, SolcStandardJsonInputSettings,
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection,
SolcStandardJsonOutput,
};
use semver::Version;
use revive_dt_common::cached_fs::read_to_string;
use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments;
// Re-export this as it's a part of the compiler interface.
pub use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
pub mod revive_js;
pub mod revive_resolc;
@@ -32,63 +36,52 @@ pub trait SolidityCompiler {
/// The low-level compiler interface.
fn build(
&self,
input: CompilerInput<Self::Options>,
) -> anyhow::Result<CompilerOutput<Self::Options>>;
input: CompilerInput,
additional_options: Self::Options,
) -> impl Future<Output = anyhow::Result<CompilerOutput>>;
fn new(solc_executable: PathBuf) -> Self;
fn get_compiler_executable(config: &Arguments, version: Version) -> anyhow::Result<PathBuf>;
fn get_compiler_executable(
config: &Arguments,
version: impl Into<VersionOrRequirement>,
) -> impl Future<Output = anyhow::Result<PathBuf>>;
fn version(&self) -> anyhow::Result<Version>;
/// Does the compiler support the provided mode and version settings?
fn supports_mode(
compiler_version: &Version,
optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool;
}
/// The generic compilation input configuration.
#[derive(Debug)]
pub struct CompilerInput<T: PartialEq + Eq + Hash> {
pub extra_options: T,
pub input: SolcStandardJsonInput,
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompilerInput {
pub pipeline: Option<ModePipeline>,
pub optimization: Option<ModeOptimizerSetting>,
pub evm_version: Option<EVMVersion>,
pub allow_paths: Vec<PathBuf>,
pub base_path: Option<PathBuf>,
pub sources: HashMap<PathBuf, String>,
pub libraries: HashMap<PathBuf, HashMap<String, Address>>,
pub revert_string_handling: Option<RevertString>,
}
/// The generic compilation output configuration.
#[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>,
#[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)>>,
}
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.
/// A generic builder style interface for configuring the supported compiler options.
pub struct Compiler<T: SolidityCompiler> {
input: SolcStandardJsonInput,
extra_options: T::Options,
allow_paths: Vec<PathBuf>,
base_path: Option<PathBuf>,
input: CompilerInput,
additional_options: T::Options,
}
impl Default for Compiler<solc::Solc> {
@@ -103,93 +96,109 @@ where
{
pub fn new() -> Self {
Self {
input: SolcStandardJsonInput {
language: SolcStandardJsonInputLanguage::Solidity,
sources: Default::default(),
settings: SolcStandardJsonInputSettings::new(
None,
Default::default(),
None,
SolcStandardJsonInputSettingsSelection::new_required(),
SolcStandardJsonInputSettingsOptimizer::new(
false,
None,
&Version::new(0, 0, 0),
false,
),
None,
None,
),
},
extra_options: Default::default(),
input: CompilerInput {
pipeline: Default::default(),
optimization: Default::default(),
evm_version: Default::default(),
allow_paths: Default::default(),
base_path: None,
base_path: Default::default(),
sources: Default::default(),
libraries: Default::default(),
revert_string_handling: Default::default(),
},
additional_options: T::Options::default(),
}
}
pub fn solc_optimizer(mut self, enabled: bool) -> Self {
self.input.settings.optimizer.enabled = enabled;
pub fn with_optimization(mut self, value: impl Into<Option<ModeOptimizerSetting>>) -> Self {
self.input.optimization = value.into();
self
}
pub fn with_source(mut self, path: &Path) -> anyhow::Result<Self> {
pub fn with_pipeline(mut self, value: impl Into<Option<ModePipeline>>) -> Self {
self.input.pipeline = 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> {
self.input
.sources
.insert(path.display().to_string(), read_to_string(path)?.into());
.insert(path.as_ref().to_path_buf(), read_to_string(path.as_ref())?);
Ok(self)
}
pub fn evm_version(mut self, evm_version: EVMVersion) -> Self {
self.input.settings.evm_version = Some(evm_version);
self
}
pub fn extra_options(mut self, extra_options: T::Options) -> Self {
self.extra_options = extra_options;
self
}
pub fn allow_path(mut self, path: PathBuf) -> Self {
self.allow_paths.push(path);
self
}
pub fn base_path(mut self, base_path: PathBuf) -> Self {
self.base_path = Some(base_path);
self
}
pub fn with_library(
mut self,
scope: impl AsRef<Path>,
library_ident: impl AsRef<str>,
library_address: Address,
path: impl AsRef<Path>,
name: impl AsRef<str>,
address: Address,
) -> Self {
self.input
.settings
.libraries
.get_or_insert_with(Default::default)
.entry(scope.as_ref().display().to_string())
.entry(path.as_ref().to_path_buf())
.or_default()
.insert(
library_ident.as_ref().to_owned(),
library_address.to_string(),
);
.insert(name.as_ref().into(), address);
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,
})
pub fn with_revert_string_handling(
mut self,
revert_string_handling: impl Into<Option<RevertString>>,
) -> Self {
self.input.revert_string_handling = revert_string_handling.into();
self
}
/// Returns the compiler JSON input.
pub fn input(&self) -> SolcStandardJsonInput {
pub fn with_additional_options(mut self, options: impl Into<T::Options>) -> Self {
self.additional_options = options.into();
self
}
pub fn then(self, callback: impl FnOnce(Self) -> Self) -> Self {
callback(self)
}
pub fn try_then<E>(self, callback: impl FnOnce(Self) -> Result<Self, E>) -> Result<Self, E> {
callback(self)
}
pub async 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)
.await
}
pub fn input(&self) -> CompilerInput {
self.input.clone()
}
}
/// Defines how the compiler should handle revert strings.
#[derive(
Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
pub enum RevertString {
#[default]
Default,
Debug,
Strip,
VerboseDebug,
}
+199 -77
View File
@@ -6,9 +6,25 @@ use std::{
process::{Command, Stdio},
};
use crate::{CompilerInput, CompilerOutput, SolidityCompiler};
use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments;
use revive_solc_json_interface::SolcStandardJsonOutput;
use revive_solc_json_interface::{
SolcStandardJsonInput, SolcStandardJsonInputLanguage, SolcStandardJsonInputSettings,
SolcStandardJsonInputSettingsOptimizer, SolcStandardJsonInputSettingsSelection,
SolcStandardJsonOutput,
};
use super::constants::SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
use alloy::json_abi::JsonAbi;
use anyhow::Context;
use semver::Version;
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
// 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.
/// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode.
#[derive(Debug)]
@@ -21,24 +37,81 @@ impl SolidityCompiler for Resolc {
type Options = Vec<String>;
#[tracing::instrument(level = "debug", ret)]
fn build(
async fn build(
&self,
input: CompilerInput<Self::Options>,
) -> anyhow::Result<CompilerOutput<Self::Options>> {
let mut command = Command::new(&self.resolc_path);
CompilerInput {
pipeline,
optimization,
evm_version,
allow_paths,
base_path,
sources,
libraries,
// TODO: this is currently not being handled since there is no way to pass it into
// resolc. So, we need to go back to this later once it's supported.
revert_string_handling: _,
}: CompilerInput,
additional_options: Self::Options,
) -> anyhow::Result<CompilerOutput> {
if !matches!(pipeline, None | Some(ModePipeline::ViaYulIR)) {
anyhow::bail!(
"Resolc only supports the Y (via Yul IR) pipeline, but the provided pipeline is {pipeline:?}"
);
}
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(
optimization
.unwrap_or(ModeOptimizerSetting::M0)
.optimizations_enabled(),
None,
&Version::new(0, 0, 0),
false,
),
metadata: None,
polkavm: None,
},
};
let mut command = AsyncCommand::new(&self.resolc_path);
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg("--standard-json");
if let Some(ref base_path) = input.base_path {
if let Some(ref base_path) = base_path {
command.arg("--base-path").arg(base_path);
}
if !input.allow_paths.is_empty() {
if !allow_paths.is_empty() {
command.arg("--allow-paths").arg(
input
.allow_paths
allow_paths
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
@@ -48,102 +121,96 @@ 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.input)?;
let serialized_input = serde_json::to_vec(&input)?;
stdin_pipe.write_all(&serialized_input).await?;
let json_in = serde_json::to_string_pretty(&input.input)?;
let output = child.wait_with_output()?;
let output = child.wait_with_output().await?;
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!(
"resolc failed exit={} stderr={} JSON-in={} ",
output.status,
&message,
json_in,
status = %output.status,
message = %message,
json_input = json_in,
"Compilation using resolc failed"
);
return Ok(CompilerOutput {
input,
output: Default::default(),
error: Some(message.into()),
});
anyhow::bail!("Compilation failed with an error: {message}");
}
let mut parsed =
serde_json::from_slice::<SolcStandardJsonOutput>(&stdout).map_err(|e| {
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)
)
})?;
// 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"
);
Ok(CompilerOutput {
input,
output: parsed,
error: None,
// 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)
}
fn new(resolc_path: PathBuf) -> Self {
Resolc { resolc_path }
}
fn get_compiler_executable(
async fn get_compiler_executable(
config: &Arguments,
_version: semver::Version,
_version: impl Into<VersionOrRequirement>,
) -> anyhow::Result<PathBuf> {
if !config.resolc.as_os_str().is_empty() {
return Ok(config.resolc.clone());
@@ -151,4 +218,59 @@ 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)
}
fn supports_mode(
compiler_version: &Version,
_optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool {
// We only support the Y (IE compile via Yul IR) mode here, which also means that we can
// only use solc version 0.8.13 and above. We must always compile via Yul IR as resolc
// needs this to translate to LLVM IR and then RISCV.
pipeline == ModePipeline::ViaYulIR
&& compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR
}
}
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
async fn compiler_version_can_be_obtained() {
// Arrange
let args = Arguments::default();
let path = Resolc::get_compiler_executable(&args, Version::new(0, 7, 6))
.await
.unwrap();
let compiler = Resolc::new(path);
// Act
let version = compiler.version();
// Assert
let _ = version.expect("Failed to get version");
}
}
+222 -29
View File
@@ -6,10 +6,23 @@ use std::{
process::{Command, Stdio},
};
use crate::{CompilerInput, CompilerOutput, SolidityCompiler};
use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments;
use revive_dt_solc_binaries::download_solc;
use revive_solc_json_interface::SolcStandardJsonOutput;
use super::constants::SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
use anyhow::Context;
use foundry_compilers_artifacts::{
output_selection::{
BytecodeOutputSelection, ContractOutputSelection, EvmOutputSelection, OutputSelection,
},
solc::CompilerOutput as SolcOutput,
solc::*,
};
use semver::Version;
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
#[derive(Debug)]
pub struct Solc {
@@ -20,24 +33,99 @@ impl SolidityCompiler for Solc {
type Options = ();
#[tracing::instrument(level = "debug", ret)]
fn build(
async fn build(
&self,
input: CompilerInput<Self::Options>,
) -> anyhow::Result<CompilerOutput<Self::Options>> {
let mut command = Command::new(&self.solc_path);
CompilerInput {
pipeline,
optimization,
evm_version,
allow_paths,
base_path,
sources,
libraries,
revert_string_handling,
}: CompilerInput,
_: Self::Options,
) -> anyhow::Result<CompilerOutput> {
let compiler_supports_via_ir = self.version()? >= SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
// Be careful to entirely omit the viaIR field if the compiler does not support it,
// as it will error if you provide fields it does not know about. Because
// `supports_mode` is called prior to instantiating a compiler, we should never
// ask for something which is invalid.
let via_ir = match (pipeline, compiler_supports_via_ir) {
(pipeline, true) => pipeline.map(|p| p.via_yul_ir()),
(_pipeline, false) => None,
};
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: optimization.map(|o| o.optimizations_enabled()),
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(),
},
debug: revert_string_handling.map(|revert_string_handling| DebuggingSettings {
revert_strings: match revert_string_handling {
crate::RevertString::Default => Some(RevertStrings::Default),
crate::RevertString::Debug => Some(RevertStrings::Debug),
crate::RevertString::Strip => Some(RevertStrings::Strip),
crate::RevertString::VerboseDebug => Some(RevertStrings::VerboseDebug),
},
debug_info: Default::default(),
}),
..Default::default()
},
};
let mut command = AsyncCommand::new(&self.solc_path);
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg("--standard-json");
if let Some(ref base_path) = input.base_path {
if let Some(ref base_path) = base_path {
command.arg("--base-path").arg(base_path);
}
if !input.allow_paths.is_empty() {
if !allow_paths.is_empty() {
command.arg("--allow-paths").arg(
input
.allow_paths
allow_paths
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
@@ -47,21 +135,23 @@ 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.input)?;
let output = child.wait_with_output()?;
let serialized_input = serde_json::to_vec(&input)?;
stdin.write_all(&serialized_input).await?;
let output = child.wait_with_output().await?;
if !output.status.success() {
let json_in = serde_json::to_string_pretty(&input)?;
let message = String::from_utf8_lossy(&output.stderr);
tracing::error!("solc failed exit={} stderr={}", output.status, &message);
return Ok(CompilerOutput {
input,
output: Default::default(),
error: Some(message.into()),
});
tracing::error!(
status = %output.status,
message = %message,
json_input = json_in,
"Compilation using solc failed"
);
anyhow::bail!("Compilation failed with an error: {message}");
}
let parsed =
serde_json::from_slice::<SolcStandardJsonOutput>(&output.stdout).map_err(|e| {
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)
@@ -70,8 +160,8 @@ impl SolidityCompiler for Solc {
// 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" {
for error in parsed.errors.iter() {
if error.severity == Severity::Error {
tracing::error!(?error, ?input, "Encountered an error in the compilation");
anyhow::bail!("Encountered an error in the compilation: {error}")
}
@@ -82,22 +172,125 @@ impl SolidityCompiler for Solc {
"Compiled successfully"
);
Ok(CompilerOutput {
input,
output: parsed,
error: None,
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)
}
fn new(solc_path: PathBuf) -> Self {
Self { solc_path }
}
fn get_compiler_executable(
async fn get_compiler_executable(
config: &Arguments,
version: semver::Version,
version: impl Into<VersionOrRequirement>,
) -> anyhow::Result<PathBuf> {
let path = download_solc(config.directory(), version, config.wasm)?;
let path = download_solc(config.directory(), version, config.wasm).await?;
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)
}
fn supports_mode(
compiler_version: &Version,
_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
&& compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR)
}
}
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
async fn compiler_version_can_be_obtained() {
// Arrange
let args = Arguments::default();
println!("Getting compiler path");
let path = Solc::get_compiler_executable(&args, Version::new(0, 7, 6))
.await
.unwrap();
println!("Got compiler path");
let compiler = Solc::new(path);
// Act
let version = compiler.version();
// Assert
assert_eq!(
version.expect("Failed to get version"),
Version::new(0, 7, 6)
)
}
#[tokio::test]
async fn compiler_version_can_be_obtained1() {
// Arrange
let args = Arguments::default();
println!("Getting compiler path");
let path = Solc::get_compiler_executable(&args, Version::new(0, 4, 21))
.await
.unwrap();
println!("Got compiler path");
let compiler = Solc::new(path);
// Act
let version = compiler.version();
// Assert
assert_eq!(
version.expect("Failed to get version"),
Version::new(0, 4, 21)
)
}
}
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.9;
contract Callable {
function f(uint[1] memory p1) public pure returns(uint) {
return p1[0];
}
}
@@ -0,0 +1,13 @@
// 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);
}
}
@@ -0,0 +1,21 @@
{ "cases": [ {
"name": "first",
"inputs": [
{
"instance": "Main",
"method": "main",
"calldata": [
"1",
"Callable.address"
]
}
],
"expected": [
"1"
]
} ],
"contracts": {
"Main": "main.sol:Main",
"Callable": "callable.sol:Callable"
}
}
+88
View File
@@ -0,0 +1,88 @@
use std::path::PathBuf;
use revive_dt_compiler::{Compiler, SolidityCompiler, revive_resolc::Resolc, solc::Solc};
use revive_dt_config::Arguments;
use semver::Version;
#[tokio::test]
async 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))
.await
.unwrap();
println!("About to assert");
// 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)
.await;
// 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"));
}
#[tokio::test]
async 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))
.await
.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)
.await;
// 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"));
}
+41 -11
View File
@@ -3,6 +3,7 @@
use std::{
fmt::Display,
path::{Path, PathBuf},
sync::LazyLock,
};
use alloy::{network::EthereumWallet, signers::local::PrivateKeySigner};
@@ -54,13 +55,9 @@ pub struct Arguments {
pub geth: PathBuf,
/// The maximum time in milliseconds to wait for geth to start.
#[arg(long = "geth-start-timeout", default_value = "2000")]
#[arg(long = "geth-start-timeout", default_value = "5000")]
pub geth_start_timeout: u64,
/// The test network chain ID.
#[arg(short, long = "network-id", default_value = "420420420")]
pub network_id: u64,
/// Configure nodes according to this genesis.json file.
#[arg(long = "genesis", default_value = "genesis.json")]
pub genesis_file: PathBuf,
@@ -76,7 +73,7 @@ pub struct Arguments {
/// This argument controls which private keys the nodes should have access to and be added to
/// its wallet signers. With a value of N, private keys (0, N] will be added to the signer set
/// of the node.
#[arg(long = "private-keys-count", default_value_t = 30)]
#[arg(long = "private-keys-count", default_value_t = 100_000)]
pub private_keys_to_add: usize,
/// The differential testing leader node implementation.
@@ -91,9 +88,22 @@ pub struct Arguments {
#[arg(long = "compile-only")]
pub compile_only: Option<TestingPlatform>,
/// Determines the amount of tests that are executed in parallel.
#[arg(long = "workers", default_value = "12")]
pub workers: usize,
/// Determines the amount of nodes that will be spawned for each chain.
#[arg(long, default_value = "1")]
pub number_of_nodes: usize,
/// Determines the amount of tokio worker threads that will will be used.
#[arg(
long,
default_value_t = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
)]
pub number_of_threads: usize,
/// Determines the amount of concurrent tasks that will be spawned to run tests. Defaults to 10 x the number of nodes.
#[arg(long)]
pub number_concurrent_tasks: Option<usize>,
/// Extract problems back to the test corpus.
#[arg(short, long = "extract-problems")]
@@ -110,6 +120,10 @@ pub struct Arguments {
/// By default it uses `eth-rpc` binary found in `$PATH`.
#[arg(short = 'p', long = "eth_proxy", default_value = "eth-rpc")]
pub eth_proxy: PathBuf,
/// Controls if the compilation cache should be invalidated or not.
#[arg(short, long)]
pub invalidate_compilation_cache: bool,
}
impl Arguments {
@@ -129,6 +143,13 @@ impl Arguments {
panic!("should have a workdir configured")
}
/// Return the number of concurrent tasks to run. This is provided via the
/// `--number-concurrent-tasks` argument, and otherwise defaults to --number-of-nodes * 20.
pub fn number_of_concurrent_tasks(&self) -> usize {
self.number_concurrent_tasks
.unwrap_or(20 * self.number_of_nodes)
}
/// Try to parse `self.account` into a [PrivateKeySigner],
/// panicing on error.
pub fn wallet(&self) -> EthereumWallet {
@@ -144,14 +165,23 @@ impl Arguments {
impl Default for Arguments {
fn default() -> Self {
Arguments::parse_from(["retester"])
static TEMP_DIR: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
let default = Arguments::parse_from(["retester"]);
Arguments {
temp_dir: Some(&TEMP_DIR),
..default
}
}
}
/// The Solidity compatible node implementation.
///
/// This describes the solutions to be tested against on a high level.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, ValueEnum, Serialize, Deserialize)]
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, ValueEnum, Serialize, Deserialize,
)]
#[clap(rename_all = "lower")]
pub enum TestingPlatform {
/// The go-ethereum reference full node EVM implementation.
+8 -2
View File
@@ -23,11 +23,17 @@ revive-dt-report = { workspace = true }
alloy = { workspace = true }
anyhow = { workspace = true }
bson = { workspace = true }
cacache = { workspace = true }
clap = { workspace = true }
futures = { workspace = true }
indexmap = { workspace = true }
once_cell = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
rayon = { workspace = true }
revive-solc-json-interface = { workspace = true }
semver = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
temp-dir = { workspace = true }
tempfile = { workspace = true }
+261
View File
@@ -0,0 +1,261 @@
//! A wrapper around the compiler which allows for caching of compilation artifacts so that they can
//! be reused between runs.
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
};
use futures::FutureExt;
use revive_dt_common::iterators::FilesWithExtensionIterator;
use revive_dt_compiler::{Compiler, CompilerOutput, Mode, SolidityCompiler};
use revive_dt_config::Arguments;
use revive_dt_format::metadata::{ContractIdent, ContractInstance, Metadata};
use alloy::{hex::ToHexExt, json_abi::JsonAbi, primitives::Address};
use anyhow::{Error, Result};
use once_cell::sync::Lazy;
use semver::Version;
use serde::{Deserialize, Serialize};
use tokio::sync::{Mutex, RwLock};
use tracing::{Instrument, debug, debug_span, instrument};
use crate::Platform;
pub struct CachedCompiler(ArtifactsCache);
impl CachedCompiler {
pub async fn new(path: impl AsRef<Path>, invalidate_cache: bool) -> Result<Self> {
let mut cache = ArtifactsCache::new(path);
if invalidate_cache {
cache = cache.with_invalidated_cache().await?;
}
Ok(Self(cache))
}
/// Compiles or gets the compilation artifacts from the cache.
#[instrument(
level = "debug",
skip_all,
fields(
metadata_file_path = %metadata_file_path.as_ref().display(),
%mode,
platform = P::config_id().to_string()
),
err
)]
pub async fn compile_contracts<P: Platform>(
&self,
metadata: &Metadata,
metadata_file_path: impl AsRef<Path>,
mode: &Mode,
config: &Arguments,
deployed_libraries: Option<&HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
) -> Result<(CompilerOutput, Version)> {
static CACHE_KEY_LOCK: Lazy<RwLock<HashMap<CacheKey, Arc<Mutex<()>>>>> =
Lazy::new(Default::default);
let compiler_version_or_requirement = mode.compiler_version_to_use(config.solc.clone());
let compiler_path = <P::Compiler as SolidityCompiler>::get_compiler_executable(
config,
compiler_version_or_requirement,
)
.await?;
let compiler_version =
<P::Compiler as SolidityCompiler>::new(compiler_path.clone()).version()?;
let cache_key = CacheKey {
platform_key: P::config_id().to_string(),
compiler_version: compiler_version.clone(),
metadata_file_path: metadata_file_path.as_ref().to_path_buf(),
solc_mode: mode.clone(),
};
let compilation_callback = || {
async move {
compile_contracts::<P>(
metadata.directory()?,
compiler_path,
metadata.files_to_compile()?,
mode,
deployed_libraries,
)
.map(|compilation_result| compilation_result.map(CacheValue::new))
.await
}
.instrument(debug_span!(
"Running compilation for the cache key",
cache_key.platform_key = %cache_key.platform_key,
cache_key.compiler_version = %cache_key.compiler_version,
cache_key.metadata_file_path = %cache_key.metadata_file_path.display(),
cache_key.solc_mode = %cache_key.solc_mode,
))
};
let compiled_contracts = match deployed_libraries {
// If deployed libraries have been specified then we will re-compile the contract as it
// means that linking is required in this case.
Some(_) => {
debug!("Deployed libraries defined, recompilation must take place");
debug!("Cache miss");
compilation_callback().await?.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 => {
debug!("Deployed libraries undefined, attempting to make use of cache");
// Lock this specific cache key such that we do not get inconsistent state. We want
// that when multiple cases come in asking for the compilation artifacts then they
// don't all trigger a compilation if there's a cache miss. Hence, the lock here.
let read_guard = CACHE_KEY_LOCK.read().await;
let mutex = match read_guard.get(&cache_key).cloned() {
Some(value) => value,
None => {
drop(read_guard);
CACHE_KEY_LOCK
.write()
.await
.entry(cache_key.clone())
.or_default()
.clone()
}
};
let _guard = mutex.lock().await;
self.0
.get_or_insert_with(&cache_key, compilation_callback)
.await
.map(|value| value.compiler_output)?
}
};
Ok((compiled_contracts, compiler_version))
}
}
async fn compile_contracts<P: Platform>(
metadata_directory: impl AsRef<Path>,
compiler_path: impl AsRef<Path>,
mut files_to_compile: impl Iterator<Item = PathBuf>,
mode: &Mode,
deployed_libraries: Option<&HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
) -> Result<CompilerOutput> {
let all_sources_in_dir = FilesWithExtensionIterator::new(metadata_directory.as_ref())
.with_allowed_extension("sol")
.with_use_cached_fs(true)
.collect::<Vec<_>>();
Compiler::<P::Compiler>::new()
.with_allow_path(metadata_directory)
// Handling the modes
.with_optimization(mode.optimize_setting)
.with_pipeline(mode.pipeline)
// Adding the contract sources to the compiler.
.try_then(|compiler| {
files_to_compile.try_fold(compiler, |compiler, path| compiler.with_source(path))
})?
// Adding the deployed libraries to the compiler.
.then(|compiler| {
deployed_libraries
.iter()
.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))
})
.fold(compiler, |compiler, (ident, address, path)| {
compiler.with_library(path, ident.as_str(), *address)
})
})
.try_build(compiler_path)
.await
}
struct ArtifactsCache {
path: PathBuf,
}
impl ArtifactsCache {
pub fn new(path: impl AsRef<Path>) -> Self {
Self {
path: path.as_ref().to_path_buf(),
}
}
#[instrument(level = "debug", skip_all, err)]
pub async fn with_invalidated_cache(self) -> Result<Self> {
cacache::clear(self.path.as_path())
.await
.map_err(Into::<Error>::into)?;
Ok(self)
}
#[instrument(level = "debug", skip_all, err)]
pub async fn insert(&self, key: &CacheKey, value: &CacheValue) -> Result<()> {
let key = bson::to_vec(key)?;
let value = bson::to_vec(value)?;
cacache::write(self.path.as_path(), key.encode_hex(), value).await?;
Ok(())
}
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 = bson::from_slice::<CacheValue>(&value).ok()?;
Some(value)
}
#[instrument(level = "debug", skip_all, err)]
pub async fn get_or_insert_with(
&self,
key: &CacheKey,
callback: impl AsyncFnOnce() -> Result<CacheValue>,
) -> Result<CacheValue> {
match self.get(key).await {
Some(value) => {
debug!("Cache hit");
Ok(value)
}
None => {
debug!("Cache miss");
let value = callback().await?;
self.insert(key, &value).await?;
Ok(value)
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct CacheKey {
/// The platform name that this artifact was compiled for. For example, this could be EVM or
/// PVM.
platform_key: String,
/// The version of the compiler that was used to compile the artifacts.
compiler_version: Version,
/// The path of the metadata file that the compilation artifacts are for.
metadata_file_path: PathBuf,
/// The mode that the compilation artifacts where compiled with.
solc_mode: Mode,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct CacheValue {
/// The compiler output from the compilation run.
compiler_output: CompilerOutput,
}
impl CacheValue {
pub fn new(compiler_output: CompilerOutput) -> Self {
Self { compiler_output }
}
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -26,7 +26,7 @@ pub trait Platform {
pub struct Geth;
impl Platform for Geth {
type Blockchain = geth::Instance;
type Blockchain = geth::GethNode;
type Compiler = solc::Solc;
fn config_id() -> TestingPlatform {
+579 -75
View File
@@ -1,37 +1,80 @@
use std::{collections::HashMap, sync::LazyLock};
mod cached_compiler;
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{Arc, LazyLock},
time::Instant,
};
use alloy::{
network::{Ethereum, TransactionBuilder},
rpc::types::TransactionRequest,
};
use anyhow::Context;
use clap::Parser;
use rayon::{ThreadPoolBuilder, prelude::*};
use futures::stream::futures_unordered::FuturesUnordered;
use futures::{Stream, StreamExt};
use revive_dt_node_interaction::EthereumNode;
use temp_dir::TempDir;
use tokio::sync::mpsc;
use tracing::{Instrument, Level};
use tracing_subscriber::{EnvFilter, FmtSubscriber};
use revive_dt_common::types::Mode;
use revive_dt_compiler::SolidityCompiler;
use revive_dt_config::*;
use revive_dt_core::{
Geth, Kitchensink, Platform,
driver::{Driver, State},
driver::{CaseDriver, CaseState},
};
use revive_dt_format::{
case::{Case, CaseIdx},
corpus::Corpus,
input::{Input, Step},
metadata::{ContractPathAndIdent, Metadata, MetadataFile},
};
use revive_dt_format::{corpus::Corpus, metadata::MetadataFile};
use revive_dt_node::pool::NodePool;
use revive_dt_report::reporter::{Report, Span};
use temp_dir::TempDir;
use tracing::Level;
use tracing_subscriber::{EnvFilter, FmtSubscriber};
use crate::cached_compiler::CachedCompiler;
static TEMP_DIR: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
/// this represents a single "test"; a mode, path and collection of cases.
#[derive(Clone)]
struct Test {
metadata: Metadata,
path: PathBuf,
mode: Mode,
case_idx: CaseIdx,
case: Case,
}
/// This represents the results that we gather from running test cases.
type CaseResult = Result<usize, anyhow::Error>;
fn main() -> anyhow::Result<()> {
let args = init_cli()?;
let body = async {
for (corpus, tests) in collect_corpora(&args)? {
let span = Span::new(corpus, args.clone())?;
match &args.compile_only {
Some(platform) => compile_corpus(&args, &tests, platform, span),
None => execute_corpus(&args, &tests, span)?,
Some(platform) => compile_corpus(&args, &tests, platform, span).await,
None => execute_corpus(&args, &tests, span).await?,
}
Report::save()?;
}
Ok(())
};
tokio::runtime::Builder::new_multi_thread()
.worker_threads(args.number_of_threads)
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(body)
}
fn init_cli() -> anyhow::Result<Arguments> {
@@ -62,10 +105,6 @@ fn init_cli() -> anyhow::Result<Arguments> {
}
tracing::info!("workdir: {}", args.directory().display());
ThreadPoolBuilder::new()
.num_threads(args.workers)
.build_global()?;
Ok(args)
}
@@ -83,71 +122,505 @@ fn collect_corpora(args: &Arguments) -> anyhow::Result<HashMap<Corpus, Vec<Metad
Ok(corpora)
}
fn run_driver<L, F>(args: &Arguments, tests: &[MetadataFile], span: Span) -> anyhow::Result<()>
async fn run_driver<L, F>(
args: &Arguments,
metadata_files: &[MetadataFile],
span: Span,
) -> anyhow::Result<()>
where
L: Platform,
F: Platform,
L::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
F::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
{
let leader_nodes = NodePool::<L::Blockchain>::new(args)?;
let follower_nodes = NodePool::<F::Blockchain>::new(args)?;
let (report_tx, report_rx) = mpsc::unbounded_channel::<(Test, CaseResult)>();
tests.par_iter().for_each(
|MetadataFile {
content: metadata,
path: metadata_file_path,
}| {
// Starting a new tracing span for this metadata file. This allows our logs to be clear
// about which metadata file the logs belong to. We can add other information into this
// as well to be able to associate the logs with the correct metadata file and case
// that's being executed.
let tracing_span = tracing::span!(
Level::INFO,
"Running driver",
metadata_file_path = metadata_file_path.display().to_string(),
);
let _guard = tracing_span.enter();
let tests = prepare_tests::<L, F>(args, metadata_files);
let driver_task = start_driver_task::<L, F>(args, tests, span, report_tx).await?;
let status_reporter_task = start_reporter_task(report_rx);
let mut driver = Driver::<L, F>::new(
metadata,
args,
leader_nodes.round_robbin(),
follower_nodes.round_robbin(),
);
let execution_result = driver.execute(span);
tracing::info!(
case_success_count = execution_result.successful_cases_count,
case_failure_count = execution_result.failed_cases_count,
"Execution completed"
);
let mut error_count = 0;
for result in execution_result.results.iter() {
if !result.is_success() {
tracing::error!(execution_error = ?result, "Encountered an error");
error_count += 1;
}
}
if error_count == 0 {
tracing::info!("Execution succeeded");
} else {
tracing::info!("Execution failed");
}
},
);
tokio::join!(status_reporter_task, driver_task);
Ok(())
}
fn execute_corpus(args: &Arguments, tests: &[MetadataFile], span: Span) -> anyhow::Result<()> {
fn prepare_tests<L, F>(
args: &Arguments,
metadata_files: &[MetadataFile],
) -> impl Stream<Item = Test>
where
L: Platform,
F: Platform,
L::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
F::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
{
metadata_files
.iter()
.flat_map(
|MetadataFile {
path,
content: metadata,
}| {
metadata
.cases
.iter()
.enumerate()
.flat_map(move |(case_idx, case)| {
metadata
.solc_modes()
.into_iter()
.map(move |solc_mode| (path, metadata, case_idx, case, solc_mode))
})
},
)
.filter(
|(metadata_file_path, metadata, _, _, _)| match metadata.ignore {
Some(true) => {
tracing::warn!(
metadata_file_path = %metadata_file_path.display(),
"Ignoring metadata file"
);
false
}
Some(false) | None => true,
},
)
.filter(
|(metadata_file_path, _, case_idx, case, _)| match case.ignore {
Some(true) => {
tracing::warn!(
metadata_file_path = %metadata_file_path.display(),
case_idx,
case_name = ?case.name,
"Ignoring case"
);
false
}
Some(false) | None => true,
},
)
.filter(|(metadata_file_path, metadata, ..)| match metadata.required_evm_version {
Some(evm_version_requirement) => {
let is_allowed = evm_version_requirement
.matches(&<L::Blockchain as revive_dt_node::Node>::evm_version())
&& evm_version_requirement
.matches(&<F::Blockchain as revive_dt_node::Node>::evm_version());
if !is_allowed {
tracing::warn!(
metadata_file_path = %metadata_file_path.display(),
leader_evm_version = %<L::Blockchain as revive_dt_node::Node>::evm_version(),
follower_evm_version = %<F::Blockchain as revive_dt_node::Node>::evm_version(),
version_requirement = %evm_version_requirement,
"Skipped test since the EVM version requirement was not fulfilled."
);
}
is_allowed
}
None => true,
})
.map(|(metadata_file_path, metadata, case_idx, case, solc_mode)| {
Test {
metadata: metadata.clone(),
path: metadata_file_path.to_path_buf(),
mode: solc_mode,
case_idx: case_idx.into(),
case: case.clone(),
}
})
.map(async |test| test)
.collect::<FuturesUnordered<_>>()
.filter_map(async move |test| {
// Check that both compilers support this test, else we skip it
let is_supported = does_compiler_support_mode::<L>(args, &test.mode).await.ok().unwrap_or(false) &&
does_compiler_support_mode::<F>(args, &test.mode).await.ok().unwrap_or(false);
// We filter_map to avoid needing to clone `test`, but return it as-is.
if is_supported {
Some(test)
} else {
tracing::warn!(
metadata_file_path = %test.path.display(),
case_idx = %test.case_idx,
case_name = ?test.case.name,
mode = %test.mode,
"Skipping test as one or both of the compilers don't support it"
);
None
}
})
}
async fn does_compiler_support_mode<P: Platform>(
args: &Arguments,
mode: &Mode,
) -> anyhow::Result<bool> {
let compiler_version_or_requirement = mode.compiler_version_to_use(args.solc.clone());
let compiler_path =
P::Compiler::get_compiler_executable(args, compiler_version_or_requirement).await?;
let compiler_version = P::Compiler::new(compiler_path.clone()).version()?;
Ok(P::Compiler::supports_mode(
&compiler_version,
mode.optimize_setting,
mode.pipeline,
))
}
async fn start_driver_task<L, F>(
args: &Arguments,
tests: impl Stream<Item = Test>,
span: Span,
report_tx: mpsc::UnboundedSender<(Test, CaseResult)>,
) -> anyhow::Result<impl Future<Output = ()>>
where
L: Platform,
F: Platform,
L::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
F::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
{
let leader_nodes = Arc::new(NodePool::<L::Blockchain>::new(args)?);
let follower_nodes = Arc::new(NodePool::<F::Blockchain>::new(args)?);
let number_concurrent_tasks = args.number_of_concurrent_tasks();
let cached_compiler = Arc::new(
CachedCompiler::new(
args.directory().join("compilation_cache"),
args.invalidate_compilation_cache,
)
.await?,
);
Ok(tests.for_each_concurrent(
// We want to limit the concurrent tasks here because:
//
// 1. We don't want to overwhelm the nodes with too many requests, leading to responses timing out.
// 2. We don't want to open too many files at once, leading to the OS running out of file descriptors.
//
// By default, we allow maximum of 10 ongoing requests per node in order to limit (1), and assume that
// this number will automatically be low enough to address (2). The user can override this.
Some(number_concurrent_tasks),
move |test| {
let leader_nodes = leader_nodes.clone();
let follower_nodes = follower_nodes.clone();
let report_tx = report_tx.clone();
let cached_compiler = cached_compiler.clone();
async move {
let leader_node = leader_nodes.round_robbin();
let follower_node = follower_nodes.round_robbin();
let tracing_span = tracing::span!(
Level::INFO,
"Running driver",
metadata_file_path = %test.path.display(),
case_idx = ?test.case_idx,
solc_mode = ?test.mode,
);
let result = handle_case_driver::<L, F>(
&test.path,
&test.metadata,
test.case_idx,
&test.case,
test.mode.clone(),
args,
cached_compiler,
leader_node,
follower_node,
span,
)
.instrument(tracing_span)
.await;
report_tx
.send((test, result))
.expect("Failed to send report");
}
},
))
}
async fn start_reporter_task(mut report_rx: mpsc::UnboundedReceiver<(Test, CaseResult)>) {
let start = Instant::now();
const GREEN: &str = "\x1B[32m";
const RED: &str = "\x1B[31m";
const COLOUR_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 failures = vec![];
// Wait for reports to come from our test runner. When the channel closes, this ends.
while let Some((test, case_result)) = report_rx.recv().await {
let case_name = test.case.name.as_deref().unwrap_or("unnamed_case");
let case_idx = test.case_idx;
let test_path = test.path.display();
let test_mode = test.mode.clone();
match case_result {
Ok(_inputs) => {
number_of_successes += 1;
eprintln!(
"{GREEN}Case Succeeded:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode})"
);
}
Err(err) => {
number_of_failures += 1;
eprintln!(
"{RED}Case Failed:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode})"
);
failures.push((test, err));
}
}
}
eprintln!();
let elapsed = start.elapsed();
// Now, log the failures with more complete errors at the bottom, like `cargo test` does, so
// that we don't have to scroll through the entire output to find them.
if !failures.is_empty() {
eprintln!("{BOLD}Failures:{BOLD_RESET}\n");
for failure in failures {
let (test, err) = failure;
let case_name = test.case.name.as_deref().unwrap_or("unnamed_case");
let case_idx = test.case_idx;
let test_path = test.path.display();
let test_mode = test.mode.clone();
eprintln!(
"---- {RED}Case Failed:{COLOUR_RESET} {test_path} -> {case_name}:{case_idx} (mode: {test_mode}) ----\n\n{err}\n"
);
}
}
// Summary at the end.
eprintln!(
"{} cases: {GREEN}{number_of_successes}{COLOUR_RESET} cases succeeded, {RED}{number_of_failures}{COLOUR_RESET} cases failed in {} seconds",
number_of_successes + number_of_failures,
elapsed.as_secs()
);
}
#[allow(clippy::too_many_arguments)]
async fn handle_case_driver<L, F>(
metadata_file_path: &Path,
metadata: &Metadata,
case_idx: CaseIdx,
case: &Case,
mode: Mode,
config: &Arguments,
cached_compiler: Arc<CachedCompiler>,
leader_node: &L::Blockchain,
follower_node: &F::Blockchain,
_: Span,
) -> anyhow::Result<usize>
where
L: Platform,
F: Platform,
L::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
F::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
{
let leader_pre_link_contracts = cached_compiler
.compile_contracts::<L>(metadata, metadata_file_path, &mode, config, None)
.await?
.0
.contracts;
let follower_pre_link_contracts = cached_compiler
.compile_contracts::<F>(metadata, metadata_file_path, &mode, config, None)
.await?
.0
.contracts;
let mut leader_deployed_libraries = None::<HashMap<_, _>>;
let mut follower_deployed_libraries = None::<HashMap<_, _>>;
let mut contract_sources = metadata.contract_sources()?;
for library_instance in metadata
.libraries
.iter()
.flatten()
.flat_map(|(_, map)| map.values())
{
let ContractPathAndIdent {
contract_source_path: library_source_path,
contract_ident: library_ident,
} = contract_sources
.remove(library_instance)
.context("Failed to find the contract source")?;
let (leader_code, leader_abi) = leader_pre_link_contracts
.get(&library_source_path)
.and_then(|contracts| contracts.get(library_ident.as_str()))
.context("Declared library was not compiled")?;
let (follower_code, follower_abi) = follower_pre_link_contracts
.get(&library_source_path)
.and_then(|contracts| contracts.get(library_ident.as_str()))
.context("Declared library was not compiled")?;
let leader_code = match alloy::hex::decode(leader_code) {
Ok(code) => code,
Err(error) => {
tracing::error!(
?error,
contract_source_path = library_source_path.display().to_string(),
contract_ident = library_ident.as_ref(),
"Failed to hex-decode byte code - This could possibly mean that the bytecode requires linking"
);
anyhow::bail!("Failed to hex-decode the byte code {}", error)
}
};
let follower_code = match alloy::hex::decode(follower_code) {
Ok(code) => code,
Err(error) => {
tracing::error!(
?error,
contract_source_path = library_source_path.display().to_string(),
contract_ident = library_ident.as_ref(),
"Failed to hex-decode byte code - This could possibly mean that the bytecode requires linking"
);
anyhow::bail!("Failed to hex-decode the byte code {}", error)
}
};
// Getting the deployer address from the cases themselves. This is to ensure that we're
// doing the deployments from different accounts and therefore we're not slowed down by
// the nonce.
let deployer_address = case
.steps
.iter()
.filter_map(|step| match step {
Step::FunctionCall(input) => Some(input.caller),
Step::BalanceAssertion(..) => None,
Step::StorageEmptyAssertion(..) => None,
})
.next()
.unwrap_or(Input::default_caller());
let leader_tx = TransactionBuilder::<Ethereum>::with_deploy_code(
TransactionRequest::default().from(deployer_address),
leader_code,
);
let follower_tx = TransactionBuilder::<Ethereum>::with_deploy_code(
TransactionRequest::default().from(deployer_address),
follower_code,
);
let leader_receipt = match leader_node.execute_transaction(leader_tx).await {
Ok(receipt) => receipt,
Err(error) => {
tracing::error!(
node = std::any::type_name::<L>(),
?error,
"Contract deployment transaction failed."
);
return Err(error);
}
};
let follower_receipt = match follower_node.execute_transaction(follower_tx).await {
Ok(receipt) => receipt,
Err(error) => {
tracing::error!(
node = std::any::type_name::<F>(),
?error,
"Contract deployment transaction failed."
);
return Err(error);
}
};
tracing::info!(
?library_instance,
library_address = ?leader_receipt.contract_address,
"Deployed library to leader"
);
tracing::info!(
?library_instance,
library_address = ?follower_receipt.contract_address,
"Deployed library to follower"
);
let Some(leader_library_address) = leader_receipt.contract_address else {
anyhow::bail!("Contract deployment didn't return an address");
};
let Some(follower_library_address) = follower_receipt.contract_address else {
anyhow::bail!("Contract deployment didn't return an address");
};
leader_deployed_libraries.get_or_insert_default().insert(
library_instance.clone(),
(
library_ident.clone(),
leader_library_address,
leader_abi.clone(),
),
);
follower_deployed_libraries.get_or_insert_default().insert(
library_instance.clone(),
(
library_ident,
follower_library_address,
follower_abi.clone(),
),
);
}
let (leader_post_link_contracts, leader_compiler_version) = cached_compiler
.compile_contracts::<L>(
metadata,
metadata_file_path,
&mode,
config,
leader_deployed_libraries.as_ref(),
)
.await?;
let (follower_post_link_contracts, follower_compiler_version) = cached_compiler
.compile_contracts::<F>(
metadata,
metadata_file_path,
&mode,
config,
follower_deployed_libraries.as_ref(),
)
.await?;
let leader_state = CaseState::<L>::new(
leader_compiler_version,
leader_post_link_contracts.contracts,
leader_deployed_libraries.unwrap_or_default(),
);
let follower_state = CaseState::<F>::new(
follower_compiler_version,
follower_post_link_contracts.contracts,
follower_deployed_libraries.unwrap_or_default(),
);
let mut driver = CaseDriver::<L, F>::new(
metadata,
case,
case_idx,
leader_node,
follower_node,
leader_state,
follower_state,
);
driver.execute().await
}
async fn execute_corpus(
args: &Arguments,
tests: &[MetadataFile],
span: Span,
) -> anyhow::Result<()> {
match (&args.leader, &args.follower) {
(TestingPlatform::Geth, TestingPlatform::Kitchensink) => {
run_driver::<Geth, Kitchensink>(args, tests, span)?
run_driver::<Geth, Kitchensink>(args, tests, span).await?
}
(TestingPlatform::Geth, TestingPlatform::Geth) => {
run_driver::<Geth, Geth>(args, tests, span)?
run_driver::<Geth, Geth>(args, tests, span).await?
}
_ => unimplemented!(),
}
@@ -155,24 +628,55 @@ fn execute_corpus(args: &Arguments, tests: &[MetadataFile], span: Span) -> anyho
Ok(())
}
fn compile_corpus(
async fn compile_corpus(
config: &Arguments,
tests: &[MetadataFile],
platform: &TestingPlatform,
span: Span,
_: Span,
) {
tests.par_iter().for_each(|metadata| {
for mode in &metadata.solc_modes() {
let tests = tests.iter().flat_map(|metadata| {
metadata
.solc_modes()
.into_iter()
.map(move |solc_mode| (metadata, solc_mode))
});
let file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
let cached_compiler = CachedCompiler::new(file.path(), false)
.await
.map(Arc::new)
.expect("Failed to create the cached compiler");
futures::stream::iter(tests)
.for_each_concurrent(None, |(metadata, mode)| {
let cached_compiler = cached_compiler.clone();
async move {
match platform {
TestingPlatform::Geth => {
let mut state = State::<Geth>::new(config, span);
let _ = state.build_contracts(mode, metadata);
let _ = cached_compiler
.compile_contracts::<Geth>(
metadata,
metadata.path.as_path(),
&mode,
config,
None,
)
.await;
}
TestingPlatform::Kitchensink => {
let mut state = State::<Kitchensink>::new(config, span);
let _ = state.build_contracts(mode, metadata);
let _ = cached_compiler
.compile_contracts::<Kitchensink>(
metadata,
metadata.path.as_path(),
&mode,
config,
None,
)
.await;
}
};
}
});
}
})
.await;
}
+6
View File
@@ -11,11 +11,17 @@ rust-version.workspace = true
[dependencies]
revive-dt-common = { workspace = true }
revive-common = { workspace = true }
alloy = { workspace = true }
alloy-primitives = { workspace = true }
alloy-sol-types = { workspace = true }
anyhow = { workspace = true }
regex = { workspace = true }
tracing = { workspace = true }
semver = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
[dev-dependencies]
tokio = { workspace = true }
+38 -13
View File
@@ -1,31 +1,50 @@
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use revive_dt_common::macros::define_wrapper_type;
use crate::{
input::{Expected, Input},
mode::Mode,
input::{Expected, Step},
mode::ParsedMode,
};
#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)]
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub struct Case {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
pub modes: Option<Vec<Mode>>,
pub inputs: Vec<Input>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modes: Option<Vec<ParsedMode>>,
#[serde(rename = "inputs")]
pub steps: Vec<Step>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expected: Option<Expected>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore: Option<bool>,
}
impl Case {
pub fn inputs_iterator(&self) -> impl Iterator<Item = Input> {
let inputs_len = self.inputs.len();
self.inputs
#[allow(irrefutable_let_patterns)]
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 input)| {
if idx + 1 == inputs_len {
.map(move |(idx, mut step)| {
let Step::FunctionCall(ref mut input) = step else {
return step;
};
if idx + 1 == steps_len {
if input.expected.is_none() {
input.expected = self.expected.clone();
}
@@ -35,9 +54,9 @@ impl Case {
// the case? What are we supposed to do with that final expected field on the
// case?
input
step
} else {
input
step
}
})
}
@@ -48,3 +67,9 @@ define_wrapper_type!(
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CaseIdx(usize);
);
impl std::fmt::Display for CaseIdx {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
+37 -5
View File
@@ -3,6 +3,7 @@ use std::{
path::{Path, PathBuf},
};
use revive_dt_common::cached_fs::read_dir;
use serde::{Deserialize, Serialize};
use crate::metadata::MetadataFile;
@@ -17,7 +18,25 @@ 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)?;
Ok(serde_json::from_reader(file)?)
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)
}
/// Scan the corpus base directory and return all tests found.
@@ -35,7 +54,8 @@ impl Corpus {
///
/// `path` is expected to be a directory.
pub fn collect_metadata(path: &Path, tests: &mut Vec<MetadataFile>) {
let dir_entry = match std::fs::read_dir(path) {
if path.is_dir() {
let dir_entry = match read_dir(path) {
Ok(dir_entry) => dir_entry,
Err(error) => {
tracing::error!("failed to read dir '{}': {error}", path.display());
@@ -43,8 +63,8 @@ pub fn collect_metadata(path: &Path, tests: &mut Vec<MetadataFile>) {
}
};
for entry in dir_entry {
let entry = match entry {
for path in dir_entry {
let path = match path {
Ok(entry) => entry,
Err(error) => {
tracing::error!("error reading dir entry: {error}");
@@ -52,7 +72,6 @@ pub fn collect_metadata(path: &Path, tests: &mut Vec<MetadataFile>) {
}
};
let path = entry.path();
if path.is_dir() {
collect_metadata(&path, tests);
continue;
@@ -64,4 +83,17 @@ pub fn collect_metadata(path: &Path, tests: &mut Vec<MetadataFile>) {
}
}
}
} else {
let Some(extension) = path.extension() else {
tracing::error!("Failed to get file extension");
return;
};
if extension.eq_ignore_ascii_case("sol") || extension.eq_ignore_ascii_case("json") {
if let Some(metadata) = MetadataFile::try_from_file(path) {
tests.push(metadata)
}
} else {
tracing::error!(?extension, "Unsupported file extension");
}
}
}
+391 -177
View File
File diff suppressed because it is too large Load Diff
+233 -33
View File
@@ -1,7 +1,8 @@
use std::{
cmp::Ordering,
collections::BTreeMap,
fmt::Display,
fs::{File, read_to_string},
fs::File,
ops::Deref,
path::{Path, PathBuf},
str::FromStr,
@@ -9,13 +10,14 @@ use std::{
use serde::{Deserialize, Serialize};
use revive_dt_common::macros::define_wrapper_type;
use crate::{
case::Case,
mode::{Mode, SolcMode},
use revive_common::EVMVersion;
use revive_dt_common::{
cached_fs::read_to_string, iterators::FilesWithExtensionIterator, macros::define_wrapper_type,
types::Mode,
};
use crate::{case::Case, mode::ParsedMode};
pub const METADATA_FILE_EXTENSION: &str = "json";
pub const SOLIDITY_CASE_FILE_EXTENSION: &str = "sol";
pub const SOLIDITY_CASE_COMMENT_MARKER: &str = "//!";
@@ -43,34 +45,51 @@ impl Deref for MetadataFile {
}
}
#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)]
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub struct Metadata {
pub targets: Option<Vec<String>>,
pub cases: Vec<Case>,
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>,
// TODO: Convert into wrapper types for clarity.
pub libraries: Option<BTreeMap<PathBuf, BTreeMap<ContractIdent, ContractInstance>>>,
/// A comment on the test case that's added for human-readability.
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore: Option<bool>,
pub modes: Option<Vec<Mode>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub targets: Option<Vec<String>>,
pub cases: Vec<Case>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub libraries: Option<BTreeMap<PathBuf, BTreeMap<ContractIdent, ContractInstance>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modes: Option<Vec<ParsedMode>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_path: Option<PathBuf>,
/// This field specifies an EVM version requirement that the test case has where the test might
/// be run of the evm version of the nodes match the evm version specified here.
#[serde(skip_serializing_if = "Option::is_none")]
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.
#[serde(skip_serializing_if = "Option::is_none")]
pub compiler_directives: Option<CompilationDirectives>,
}
impl Metadata {
/// Returns the solc modes of this metadata, inserting a default mode if not present.
pub fn solc_modes(&self) -> Vec<SolcMode> {
self.modes
.to_owned()
.unwrap_or_else(|| vec![Mode::Solidity(Default::default())])
.iter()
.filter_map(|mode| match mode {
Mode::Solidity(solc_mode) => Some(solc_mode),
Mode::Unknown(mode) => {
tracing::debug!("compiler: ignoring unknown mode '{mode}'");
None
/// Returns the modes that we should test from this metadata.
pub fn solc_modes(&self) -> Vec<Mode> {
match &self.modes {
Some(modes) => ParsedMode::many_to_modes(modes.iter()).collect(),
None => Mode::all().collect(),
}
})
.cloned()
.collect()
}
/// Returns the base directory of this metadata.
@@ -193,7 +212,7 @@ impl Metadata {
metadata.file_path = Some(path.to_path_buf());
metadata.contracts = Some(
[(
ContractInstance::new("test"),
ContractInstance::new("Test"),
ContractPathAndIdent {
contract_source_path: path.to_path_buf(),
contract_ident: ContractIdent::new("Test"),
@@ -212,6 +231,31 @@ 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")
.with_use_cached_fs(true),
))
}
}
}
define_wrapper_type!(
@@ -287,17 +331,23 @@ impl FromStr for ContractPathAndIdent {
identifier = Some(next_item.to_owned())
}
}
let Some(path) = path else {
anyhow::bail!("Path is not defined");
};
let Some(identifier) = identifier else {
anyhow::bail!("Contract identifier is not defined")
match (path, identifier) {
(Some(path), Some(identifier)) => Ok(Self {
contract_source_path: PathBuf::from(path),
contract_ident: ContractIdent::new(identifier),
}),
(None, Some(path)) | (Some(path), None) => {
let Some(identifier) = path.split(".").next().map(ToOwned::to_owned) else {
anyhow::bail!("Failed to find identifier");
};
Ok(Self {
contract_source_path: PathBuf::from(path),
contract_ident: ContractIdent::new(identifier),
})
}
(None, None) => anyhow::bail!("Failed to find the path and identifier"),
}
}
}
impl TryFrom<String> for ContractPathAndIdent {
@@ -314,6 +364,156 @@ impl From<ContractPathAndIdent> for String {
}
}
/// An EVM version requirement that the test case has. This gets serialized and
/// deserialized from and into [`String`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct EvmVersionRequirement {
ordering: Ordering,
or_equal: bool,
evm_version: EVMVersion,
}
impl EvmVersionRequirement {
pub fn new_greater_than_or_equals(version: EVMVersion) -> Self {
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,
}
}
pub fn new_equals(version: EVMVersion) -> Self {
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,
}
}
pub fn new_less_than_or_equals(version: EVMVersion) -> Self {
Self {
ordering: Ordering::Less,
or_equal: true,
evm_version: version,
}
}
pub fn matches(&self, other: &EVMVersion) -> bool {
let ordering = other.cmp(&self.evm_version);
ordering == self.ordering || (self.or_equal && matches!(ordering, Ordering::Equal))
}
}
impl Display for EvmVersionRequirement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
ordering,
or_equal,
evm_version,
} = self;
match ordering {
Ordering::Less => write!(f, "<")?,
Ordering::Equal => write!(f, "=")?,
Ordering::Greater => write!(f, ">")?,
}
if *or_equal && !matches!(ordering, Ordering::Equal) {
write!(f, "=")?;
}
write!(f, "{evm_version}")
}
}
impl FromStr for EvmVersionRequirement {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.as_bytes() {
[b'>', b'=', remaining @ ..] => Ok(Self {
ordering: Ordering::Greater,
or_equal: true,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
[b'>', remaining @ ..] => Ok(Self {
ordering: Ordering::Greater,
or_equal: false,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
[b'<', b'=', remaining @ ..] => Ok(Self {
ordering: Ordering::Less,
or_equal: true,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
[b'<', remaining @ ..] => Ok(Self {
ordering: Ordering::Less,
or_equal: false,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
[b'=', remaining @ ..] => Ok(Self {
ordering: Ordering::Equal,
or_equal: false,
evm_version: str::from_utf8(remaining)?.try_into()?,
}),
_ => anyhow::bail!("Invalid EVM version requirement {s}"),
}
}
}
impl TryFrom<String> for EvmVersionRequirement {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl From<EvmVersionRequirement> for String {
fn from(value: EvmVersionRequirement) -> Self {
value.to_string()
}
}
/// 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.
/// Defines how the compiler should handle revert strings.
#[derive(
Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
pub struct CompilationDirectives {
/// Defines how the revert strings should be handled.
pub revert_string_handling: Option<RevertString>,
}
/// Defines how the compiler should handle revert strings.
#[derive(
Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
#[serde(rename_all = "camelCase")]
pub enum RevertString {
#[default]
Default,
Debug,
Strip,
VerboseDebug,
}
#[cfg(test)]
mod test {
use super::*;
+246 -80
View File
@@ -1,96 +1,262 @@
use semver::Version;
use serde::de::Deserializer;
use regex::Regex;
use revive_dt_common::types::{Mode, ModeOptimizerSetting, ModePipeline};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt::Display;
use std::str::FromStr;
use std::sync::LazyLock;
/// Specifies the compilation mode of the test artifact.
#[derive(Hash, Debug, Clone, Eq, PartialEq)]
pub enum Mode {
Solidity(SolcMode),
Unknown(String),
/// This represents a mode that has been parsed from test metadata.
///
/// Mode strings can take the following form (in pseudo-regex):
///
/// ```text
/// [YEILV][+-]? (M[0123sz])? <semver>?
/// ```
///
/// We can parse valid mode strings into [`ParsedMode`] using [`ParsedMode::from_str`].
#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(try_from = "String", into = "String")]
pub struct ParsedMode {
pub pipeline: Option<ModePipeline>,
pub optimize_flag: Option<bool>,
pub optimize_setting: Option<ModeOptimizerSetting>,
pub version: Option<semver::VersionReq>,
}
/// Specify Solidity specific compiler options.
#[derive(Hash, Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct SolcMode {
pub solc_version: Option<semver::VersionReq>,
solc_optimize: Option<bool>,
pub llvm_optimizer_settings: Vec<String>,
}
impl FromStr for ParsedMode {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?x)
^
(?:(?P<pipeline>[YEILV])(?P<optimize_flag>[+-])?)? # Pipeline to use eg Y, E+, E-
\s*
(?P<optimize_setting>M[a-zA-Z0-9])? # Optimize setting eg M0, Ms, Mz
\s*
(?P<version>[>=<]*\d+(?:\.\d+)*)? # Optional semver version eg >=0.8.0, 0.7, <0.8
$
").unwrap()
});
impl SolcMode {
/// Try to parse a mode string into a solc mode.
/// Returns `None` if the string wasn't a solc YUL mode string.
///
/// The mode string is expected to start with the `Y` ID (YUL ID),
/// optionally followed by `+` or `-` for the solc optimizer settings.
///
/// Options can be separated by a whitespace contain the following
/// - A solc `SemVer version requirement` string
/// - One or more `-OX` where X is a supposed to be an LLVM opt mode
pub fn parse_from_mode_string(mode_string: &str) -> Option<Self> {
let mut result = Self::default();
let mut parts = mode_string.trim().split(" ");
match parts.next()? {
"Y" => {}
"Y+" => result.solc_optimize = Some(true),
"Y-" => result.solc_optimize = Some(false),
_ => return None,
}
for part in parts {
if let Ok(solc_version) = semver::VersionReq::parse(part) {
result.solc_version = Some(solc_version);
continue;
}
if let Some(level) = part.strip_prefix("-O") {
result.llvm_optimizer_settings.push(level.to_string());
continue;
}
panic!("the YUL mode string {mode_string} failed to parse, invalid part: {part}")
}
Some(result)
}
/// Returns whether to enable the solc optimizer.
pub fn solc_optimize(&self) -> bool {
self.solc_optimize.unwrap_or(true)
}
/// Calculate the latest matching solc patch version. Returns:
/// - `latest_supported` if no version request was specified.
/// - A matching version with the same minor version as `latest_supported`, if any.
/// - `None` if no minor version of the `latest_supported` version matches.
pub fn last_patch_version(&self, latest_supported: &Version) -> Option<Version> {
let Some(version_req) = self.solc_version.as_ref() else {
return Some(latest_supported.to_owned());
let Some(caps) = REGEX.captures(s) else {
anyhow::bail!("Cannot parse mode '{s}' from string");
};
// lgtm
for patch in (0..latest_supported.patch + 1).rev() {
let version = Version::new(0, latest_supported.minor, patch);
if version_req.matches(&version) {
return Some(version);
}
}
let pipeline = match caps.name("pipeline") {
Some(m) => Some(ModePipeline::from_str(m.as_str())?),
None => None,
};
None
let optimize_flag = caps.name("optimize_flag").map(|m| m.as_str() == "+");
let optimize_setting = match caps.name("optimize_setting") {
Some(m) => Some(ModeOptimizerSetting::from_str(m.as_str())?),
None => None,
};
let version = match caps.name("version") {
Some(m) => Some(semver::VersionReq::parse(m.as_str()).map_err(|e| {
anyhow::anyhow!("Cannot parse the version requirement '{}': {e}", m.as_str())
})?),
None => None,
};
Ok(ParsedMode {
pipeline,
optimize_flag,
optimize_setting,
version,
})
}
}
impl<'de> Deserialize<'de> for Mode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let mode_string = String::deserialize(deserializer)?;
impl Display for ParsedMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut has_written = false;
if let Some(solc_mode) = SolcMode::parse_from_mode_string(&mode_string) {
return Ok(Self::Solidity(solc_mode));
if let Some(pipeline) = self.pipeline {
pipeline.fmt(f)?;
if let Some(optimize_flag) = self.optimize_flag {
f.write_str(if optimize_flag { "+" } else { "-" })?;
}
has_written = true;
}
Ok(Self::Unknown(mode_string))
if let Some(optimize_setting) = self.optimize_setting {
if has_written {
f.write_str(" ")?;
}
optimize_setting.fmt(f)?;
has_written = true;
}
if let Some(version) = &self.version {
if has_written {
f.write_str(" ")?;
}
version.fmt(f)?;
}
Ok(())
}
}
impl From<ParsedMode> for String {
fn from(parsed_mode: ParsedMode) -> Self {
parsed_mode.to_string()
}
}
impl TryFrom<String> for ParsedMode {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
ParsedMode::from_str(&value)
}
}
impl ParsedMode {
/// This takes a [`ParsedMode`] and expands it into a list of [`Mode`]s that we should try.
pub fn to_modes(&self) -> impl Iterator<Item = Mode> {
let pipeline_iter = self.pipeline.as_ref().map_or_else(
|| EitherIter::A(ModePipeline::test_cases()),
|p| EitherIter::B(std::iter::once(*p)),
);
let optimize_flag_setting = self.optimize_flag.map(|flag| {
if flag {
ModeOptimizerSetting::M3
} else {
ModeOptimizerSetting::M0
}
});
let optimize_flag_iter = match optimize_flag_setting {
Some(setting) => EitherIter::A(std::iter::once(setting)),
None => EitherIter::B(ModeOptimizerSetting::test_cases()),
};
let optimize_settings_iter = self.optimize_setting.as_ref().map_or_else(
|| EitherIter::A(optimize_flag_iter),
|s| EitherIter::B(std::iter::once(*s)),
);
pipeline_iter.flat_map(move |pipeline| {
optimize_settings_iter
.clone()
.map(move |optimize_setting| Mode {
pipeline,
optimize_setting,
version: self.version.clone(),
})
})
}
/// Return a set of [`Mode`]s that correspond to the given [`ParsedMode`]s.
/// This avoids any duplicate entries.
pub fn many_to_modes<'a>(
parsed: impl Iterator<Item = &'a ParsedMode>,
) -> impl Iterator<Item = Mode> {
let modes: HashSet<_> = parsed.flat_map(|p| p.to_modes()).collect();
modes.into_iter()
}
}
/// An iterator that could be either of two iterators.
#[derive(Clone, Debug)]
enum EitherIter<A, B> {
A(A),
B(B),
}
impl<A, B> Iterator for EitherIter<A, B>
where
A: Iterator,
B: Iterator<Item = A::Item>,
{
type Item = A::Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
EitherIter::A(iter) => iter.next(),
EitherIter::B(iter) => iter.next(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parsed_mode_from_str() {
let strings = vec![
("Mz", "Mz"),
("Y", "Y"),
("Y+", "Y+"),
("Y-", "Y-"),
("E", "E"),
("E+", "E+"),
("E-", "E-"),
("Y M0", "Y M0"),
("Y M1", "Y M1"),
("Y M2", "Y M2"),
("Y M3", "Y M3"),
("Y Ms", "Y Ms"),
("Y Mz", "Y Mz"),
("E M0", "E M0"),
("E M1", "E M1"),
("E M2", "E M2"),
("E M3", "E M3"),
("E Ms", "E Ms"),
("E Mz", "E Mz"),
// When stringifying semver again, 0.8.0 becomes ^0.8.0 (same meaning)
("Y 0.8.0", "Y ^0.8.0"),
("E+ 0.8.0", "E+ ^0.8.0"),
("Y M3 >=0.8.0", "Y M3 >=0.8.0"),
("E Mz <0.7.0", "E Mz <0.7.0"),
// We can parse +- _and_ M1/M2 but the latter takes priority.
("Y+ M1 0.8.0", "Y+ M1 ^0.8.0"),
("E- M2 0.7.0", "E- M2 ^0.7.0"),
// We don't see this in the wild but it is parsed.
("<=0.8", "<=0.8"),
];
for (actual, expected) in strings {
let parsed = ParsedMode::from_str(actual)
.expect(format!("Failed to parse mode string '{actual}'").as_str());
assert_eq!(
expected,
parsed.to_string(),
"Mode string '{actual}' did not parse to '{expected}': got '{parsed}'"
);
}
}
#[test]
fn test_parsed_mode_to_test_modes() {
let strings = vec![
("Mz", vec!["Y Mz", "E Mz"]),
("Y", vec!["Y M0", "Y M3"]),
("E", vec!["E M0", "E M3"]),
("Y+", vec!["Y M3"]),
("Y-", vec!["Y M0"]),
("Y <=0.8", vec!["Y M0 <=0.8", "Y M3 <=0.8"]),
(
"<=0.8",
vec!["Y M0 <=0.8", "Y M3 <=0.8", "E M0 <=0.8", "E M3 <=0.8"],
),
];
for (actual, expected) in strings {
let parsed = ParsedMode::from_str(actual)
.expect(format!("Failed to parse mode string '{actual}'").as_str());
let expected_set: HashSet<_> = expected.into_iter().map(|s| s.to_owned()).collect();
let actual_set: HashSet<_> = parsed.to_modes().map(|m| m.to_string()).collect();
assert_eq!(
expected_set, actual_set,
"Mode string '{actual}' did not expand to '{expected_set:?}': got '{actual_set:?}'"
);
}
}
}
+134 -7
View File
@@ -1,30 +1,157 @@
use std::collections::HashMap;
use alloy::eips::BlockNumberOrTag;
use alloy::json_abi::JsonAbi;
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
use alloy_primitives::TxHash;
use anyhow::Result;
use crate::metadata::{ContractIdent, ContractInstance};
/// 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>;
fn chain_id(&self) -> impl Future<Output = Result<ChainId>>;
/// Returns the gas price for the specified transaction.
fn transaction_gas_price(&self, tx_hash: &TxHash) -> impl Future<Output = Result<u128>>;
// 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>;
fn block_gas_limit(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<u128>>;
/// Returns the coinbase of the specified block.
fn block_coinbase(&self, number: BlockNumberOrTag) -> Result<Address>;
fn block_coinbase(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<Address>>;
/// Returns the difficulty of the specified block.
fn block_difficulty(&self, number: BlockNumberOrTag) -> Result<U256>;
fn block_difficulty(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<U256>>;
/// Returns the base fee of the specified block.
fn block_base_fee(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<u64>>;
/// Returns the hash of the specified block.
fn block_hash(&self, number: BlockNumberOrTag) -> Result<BlockHash>;
fn block_hash(&self, number: BlockNumberOrTag) -> impl Future<Output = Result<BlockHash>>;
/// Returns the timestamp of the specified block,
fn block_timestamp(&self, number: BlockNumberOrTag) -> Result<BlockTimestamp>;
fn block_timestamp(
&self,
number: BlockNumberOrTag,
) -> impl Future<Output = Result<BlockTimestamp>>;
/// Returns the number of the last block.
fn last_block_number(&self) -> Result<BlockNumber>;
fn last_block_number(&self) -> impl Future<Output = Result<BlockNumber>>;
}
#[derive(Clone, Copy, Debug, Default)]
/// Contextual information required by the code that's performing the resolution.
pub struct ResolutionContext<'a> {
/// When provided the contracts provided here will be used for resolutions.
deployed_contracts: Option<&'a HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
/// When provided the variables in here will be used for performing resolutions.
variables: Option<&'a HashMap<String, U256>>,
/// When provided this block number will be treated as the tip of the chain.
block_number: Option<&'a BlockNumber>,
/// When provided the resolver will use this transaction hash for all of its resolutions.
transaction_hash: Option<&'a TxHash>,
}
impl<'a> ResolutionContext<'a> {
pub fn new() -> Self {
Default::default()
}
pub fn new_from_parts(
deployed_contracts: impl Into<
Option<&'a HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
>,
variables: impl Into<Option<&'a HashMap<String, U256>>>,
block_number: impl Into<Option<&'a BlockNumber>>,
transaction_hash: impl Into<Option<&'a TxHash>>,
) -> Self {
Self {
deployed_contracts: deployed_contracts.into(),
variables: variables.into(),
block_number: block_number.into(),
transaction_hash: transaction_hash.into(),
}
}
pub fn with_deployed_contracts(
mut self,
deployed_contracts: impl Into<
Option<&'a HashMap<ContractInstance, (ContractIdent, Address, JsonAbi)>>,
>,
) -> Self {
self.deployed_contracts = deployed_contracts.into();
self
}
pub fn with_variables(
mut self,
variables: impl Into<Option<&'a HashMap<String, U256>>>,
) -> Self {
self.variables = variables.into();
self
}
pub fn with_block_number(mut self, block_number: impl Into<Option<&'a BlockNumber>>) -> Self {
self.block_number = block_number.into();
self
}
pub fn with_transaction_hash(
mut self,
transaction_hash: impl Into<Option<&'a TxHash>>,
) -> Self {
self.transaction_hash = transaction_hash.into();
self
}
pub fn resolve_block_number(&self, number: BlockNumberOrTag) -> BlockNumberOrTag {
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,
},
None => number,
}
}
pub fn deployed_contract(
&self,
instance: &ContractInstance,
) -> Option<&(ContractIdent, Address, JsonAbi)> {
self.deployed_contracts
.and_then(|deployed_contracts| deployed_contracts.get(instance))
}
pub fn deployed_contract_address(&self, instance: &ContractInstance) -> Option<&Address> {
self.deployed_contract(instance).map(|(_, a, _)| a)
}
pub fn deployed_contract_abi(&self, instance: &ContractInstance) -> Option<&JsonAbi> {
self.deployed_contract(instance).map(|(_, _, a)| a)
}
pub fn variable(&self, name: impl AsRef<str>) -> Option<&U256> {
self.variables
.and_then(|variables| variables.get(name.as_ref()))
}
pub fn tip_block_number(&self) -> Option<&'a BlockNumber> {
self.block_number
}
pub fn transaction_hash(&self) -> Option<&'a TxHash> {
self.transaction_hash
}
}
+18 -4
View File
@@ -1,21 +1,35 @@
//! This crate implements all node interactions.
use alloy::primitives::{Address, StorageKey, U256};
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
use alloy::rpc::types::{TransactionReceipt, TransactionRequest};
use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest};
use anyhow::Result;
/// An interface for all interactions with Ethereum compatible nodes.
pub trait EthereumNode {
/// Execute the [TransactionRequest] and return a [TransactionReceipt].
fn execute_transaction(&self, transaction: TransactionRequest) -> Result<TransactionReceipt>;
fn execute_transaction(
&self,
transaction: TransactionRequest,
) -> impl Future<Output = Result<TransactionReceipt>>;
/// Trace the transaction in the [TransactionReceipt] and return a [GethTrace].
fn trace_transaction(
&self,
receipt: &TransactionReceipt,
trace_options: GethDebugTracingOptions,
) -> Result<GethTrace>;
) -> impl Future<Output = Result<GethTrace>>;
/// Returns the state diff of the transaction hash in the [TransactionReceipt].
fn state_diff(&self, receipt: &TransactionReceipt) -> Result<DiffMode>;
fn state_diff(&self, receipt: &TransactionReceipt) -> impl Future<Output = Result<DiffMode>>;
/// Returns the balance of the provided [`Address`] back.
fn balance_of(&self, address: Address) -> impl Future<Output = Result<U256>>;
/// Returns the latest storage proof of the provided [`Address`]
fn latest_state_proof(
&self,
address: Address,
keys: Vec<StorageKey>,
) -> impl Future<Output = Result<EIP1186AccountProofResponse>>;
}
+1
View File
@@ -14,6 +14,7 @@ alloy = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true }
revive-common = { workspace = true }
revive-dt-common = { workspace = true }
revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true }
+224 -153
View File
@@ -3,9 +3,13 @@
use std::{
fs::{File, OpenOptions, create_dir_all, remove_dir_all},
io::{BufRead, BufReader, Read, Write},
ops::ControlFlow,
path::PathBuf,
process::{Child, Command, Stdio},
sync::atomic::{AtomicU32, Ordering},
sync::{
Arc,
atomic::{AtomicU32, Ordering},
},
time::{Duration, Instant},
};
@@ -13,23 +17,28 @@ use alloy::{
eips::BlockNumberOrTag,
genesis::{Genesis, GenesisAccount},
network::{Ethereum, EthereumWallet, NetworkWallet},
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, FixedBytes, U256},
primitives::{
Address, BlockHash, BlockNumber, BlockTimestamp, FixedBytes, StorageKey, TxHash, U256,
},
providers::{
Provider, ProviderBuilder,
ext::DebugApi,
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
},
rpc::types::{
TransactionReceipt, TransactionRequest,
EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
},
signers::local::PrivateKeySigner,
};
use revive_dt_common::concepts::BlockingExecutor;
use anyhow::Context;
use revive_common::EVMVersion;
use tracing::{Instrument, Level};
use revive_dt_common::{fs::clear_directory, futures::poll};
use revive_dt_config::Arguments;
use revive_dt_format::traits::ResolverApi;
use revive_dt_node_interaction::EthereumNode;
use tracing::Level;
use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
@@ -43,7 +52,7 @@ static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
///
/// Prunes the child process and the base directory on drop.
#[derive(Debug)]
pub struct Instance {
pub struct GethNode {
connection_string: String,
base_directory: PathBuf,
data_directory: PathBuf,
@@ -51,7 +60,6 @@ pub struct Instance {
geth: PathBuf,
id: u32,
handle: Option<Child>,
network_id: u64,
start_timeout: u64,
wallet: EthereumWallet,
nonce_manager: CachedNonceManager,
@@ -62,7 +70,7 @@ pub struct Instance {
logs_file_to_flush: Vec<File>,
}
impl Instance {
impl GethNode {
const BASE_DIRECTORY: &str = "geth";
const DATA_DIRECTORY: &str = "data";
const LOGS_DIRECTORY: &str = "logs";
@@ -77,10 +85,17 @@ impl Instance {
const GETH_STDERR_LOG_FILE_NAME: &str = "node_stderr.log";
const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress";
const TRANSACTION_TRACING_ERROR: &str = "historical state not available in path scheme yet";
const RECEIPT_POLLING_DURATION: Duration = Duration::from_secs(5 * 60);
const TRACE_POLLING_DURATION: Duration = Duration::from_secs(60);
/// Create the node directory and call `geth init` to configure the genesis.
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn init(&mut self, genesis: String) -> anyhow::Result<&mut Self> {
let _ = clear_directory(&self.base_directory);
let _ = clear_directory(&self.logs_directory);
create_dir_all(&self.base_directory)?;
create_dir_all(&self.logs_directory)?;
@@ -99,6 +114,8 @@ impl Instance {
serde_json::to_writer(File::create(&genesis_path)?, &genesis)?;
let mut child = Command::new(&self.geth)
.arg("--state.scheme")
.arg("hash")
.arg("init")
.arg("--datadir")
.arg(&self.data_directory)
@@ -124,7 +141,7 @@ impl Instance {
/// Spawn the go-ethereum node child process.
///
/// [Instance::init] must be called prior.
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn spawn_process(&mut self) -> anyhow::Result<&mut Self> {
// This is the `OpenOptions` that we wish to use for all of the log files that we will be
// opening in this method. We need to construct it in this way to:
@@ -147,11 +164,19 @@ impl Instance {
.arg(&self.data_directory)
.arg("--ipcpath")
.arg(&self.connection_string)
.arg("--networkid")
.arg(self.network_id.to_string())
.arg("--nodiscover")
.arg("--maxpeers")
.arg("0")
.arg("--txlookuplimit")
.arg("0")
.arg("--cache.blocklogs")
.arg("512")
.arg("--state.scheme")
.arg("hash")
.arg("--syncmode")
.arg("full")
.arg("--gcmode")
.arg("archive")
.stderr(stderr_logs_file.try_clone()?)
.stdout(stdout_logs_file.try_clone()?)
.spawn()?
@@ -172,7 +197,7 @@ impl Instance {
/// Wait for the g-ethereum node child process getting ready.
///
/// [Instance::spawn_process] must be called priorly.
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn wait_ready(&mut self) -> anyhow::Result<&mut Self> {
let start_time = Instant::now();
@@ -185,6 +210,7 @@ impl Instance {
let maximum_wait_time = Duration::from_millis(self.start_timeout);
let mut stderr = BufReader::new(logs_file).lines();
let mut lines = vec![];
loop {
if let Some(Ok(line)) = stderr.next() {
if line.contains(Self::ERROR_MARKER) {
@@ -193,9 +219,14 @@ impl Instance {
if line.contains(Self::READY_MARKER) {
return Ok(self);
}
lines.push(line);
}
if Instant::now().duration_since(start_time) > maximum_wait_time {
anyhow::bail!("Timeout in starting geth");
anyhow::bail!(
"Timeout in starting geth: took longer than {}ms. stdout:\n\n{}\n",
self.start_timeout,
lines.join("\n")
);
}
}
}
@@ -229,7 +260,11 @@ impl Instance {
Box::pin(async move {
ProviderBuilder::new()
.disable_recommended_fillers()
.filler(FallbackGasFiller::new(500_000_000, 500_000_000, 1))
.filler(FallbackGasFiller::new(
25_000_000,
1_000_000_000,
1_000_000_000,
))
.filler(ChainIdFiller::default())
.filler(NonceFiller::new(nonce_manager))
.wallet(wallet)
@@ -240,24 +275,14 @@ impl Instance {
}
}
impl EthereumNode for Instance {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn execute_transaction(
impl EthereumNode for GethNode {
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
async fn execute_transaction(
&self,
transaction: TransactionRequest,
) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> {
let provider = self.provider();
BlockingExecutor::execute(async move {
let outer_span = tracing::debug_span!("Submitting transaction", ?transaction);
let _outer_guard = outer_span.enter();
let provider = provider.await?;
let pending_transaction = provider.send_transaction(transaction).await?;
let transaction_hash = pending_transaction.tx_hash();
let span = tracing::info_span!("Awaiting transaction receipt", ?transaction_hash);
let _guard = span.enter();
let provider = Arc::new(self.provider().await?);
let transaction_hash = *provider.send_transaction(transaction).await?.tx_hash();
// The following is a fix for the "transaction indexing is in progress" error that we
// used to get. You can find more information on this in the following GH issue in geth
@@ -277,160 +302,203 @@ impl EthereumNode for Instance {
// allow for a larger wait time. Therefore, in here we allow for 5 minutes of waiting
// with exponential backoff each time we attempt to get the receipt and find that it's
// not available.
let 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)) => break Ok(receipt),
Ok(None) => {}
poll(
Self::RECEIPT_POLLING_DURATION,
Default::default(),
move || {
let provider = provider.clone();
async move {
match provider.get_transaction_receipt(transaction_hash).await {
Ok(Some(receipt)) => Ok(ControlFlow::Break(receipt)),
Ok(None) => Ok(ControlFlow::Continue(())),
Err(error) => {
let error_string = error.to_string();
if !error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
break Err(error.into());
match error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
true => Ok(ControlFlow::Continue(())),
false => 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;
}
})?
}
},
)
.instrument(tracing::info_span!(
"Awaiting transaction receipt",
?transaction_hash
))
.await
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn trace_transaction(
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
async fn trace_transaction(
&self,
transaction: &TransactionReceipt,
trace_options: GethDebugTracingOptions,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
let tx_hash = transaction.transaction_hash;
let provider = self.provider();
BlockingExecutor::execute(async move {
Ok(provider
.await?
.debug_trace_transaction(tx_hash, trace_options)
.await?)
})?
let provider = Arc::new(self.provider().await?);
poll(
Self::TRACE_POLLING_DURATION,
Default::default(),
move || {
let provider = provider.clone();
let trace_options = trace_options.clone();
async move {
match provider
.debug_trace_transaction(transaction.transaction_hash, trace_options)
.await
{
Ok(trace) => Ok(ControlFlow::Break(trace)),
Err(error) => {
let error_string = error.to_string();
match error_string.contains(Self::TRANSACTION_TRACING_ERROR) {
true => Ok(ControlFlow::Continue(())),
false => Err(error.into()),
}
}
}
}
},
)
.await
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
async fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
match self
.trace_transaction(transaction, trace_options)?
.trace_transaction(transaction, trace_options)
.await?
.try_into_pre_state_frame()?
{
PreStateFrame::Diff(diff) => Ok(diff),
_ => 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();
BlockingExecutor::execute(async move {
provider.await?.get_chain_id().await.map_err(Into::into)
})?
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
async fn balance_of(&self, address: Address) -> anyhow::Result<U256> {
self.provider()
.await?
.get_balance(address)
.await
.map_err(Into::into)
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
async fn latest_state_proof(
&self,
address: Address,
keys: Vec<StorageKey>,
) -> anyhow::Result<EIP1186AccountProofResponse> {
self.provider()
.await?
.get_proof(address, keys)
.latest()
.await
.map_err(Into::into)
}
}
impl ResolverApi for GethNode {
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
async fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
self.provider()
.await?
.get_chain_id()
.await
.map_err(Into::into)
}
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
async fn transaction_gas_price(&self, tx_hash: &TxHash) -> anyhow::Result<u128> {
self.provider()
.await?
.get_transaction_receipt(*tx_hash)
.await?
.context("Failed to get the transaction receipt")
.map(|receipt| receipt.effective_gas_price)
}
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
async fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.gas_limit as _)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
async fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.beneficiary)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
async fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.difficulty)
})?
.map(|block| U256::from_be_bytes(block.header.mix_hash.0))
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
async fn block_base_fee(&self, number: BlockNumberOrTag) -> anyhow::Result<u64> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.and_then(|block| {
block
.header
.base_fee_per_gas
.context("Failed to get the base fee per gas")
})
}
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
async fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.hash)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
async fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.timestamp)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider.await?.get_block_number().await.map_err(Into::into)
})?
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
async fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
self.provider()
.await?
.get_block_number()
.await
.map_err(Into::into)
}
}
impl Node for Instance {
impl Node for GethNode {
fn new(config: &Arguments) -> Self {
let geth_directory = config.directory().join(Self::BASE_DIRECTORY);
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
@@ -453,7 +521,6 @@ impl Node for Instance {
geth: config.geth.clone(),
id,
handle: None,
network_id: config.network_id,
start_timeout: config.geth_start_timeout,
wallet,
// We know that we only need to be storing 2 files so we can specify that when creating
@@ -463,12 +530,12 @@ impl Node for Instance {
}
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn connection_string(&self) -> String {
self.connection_string.clone()
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
fn shutdown(&mut self) -> anyhow::Result<()> {
// Terminate the processes in a graceful manner to allow for the output to be flushed.
if let Some(mut child) = self.handle.take() {
@@ -490,13 +557,13 @@ impl Node for Instance {
Ok(())
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
fn spawn(&mut self, genesis: String) -> anyhow::Result<()> {
self.init(genesis)?.spawn_process()?;
Ok(())
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id), err)]
fn version(&self) -> anyhow::Result<String> {
let output = Command::new(&self.geth)
.arg("--version")
@@ -509,17 +576,21 @@ impl Node for Instance {
Ok(String::from_utf8_lossy(&output).into())
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn matches_target(&self, targets: Option<&[String]>) -> bool {
match targets {
None => true,
Some(targets) => targets.iter().any(|str| str.as_str() == "evm"),
}
}
fn evm_version() -> EVMVersion {
EVMVersion::Cancun
}
}
impl Drop for Instance {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
impl Drop for GethNode {
#[tracing::instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn drop(&mut self) {
self.shutdown().expect("Failed to shutdown")
}
@@ -543,9 +614,9 @@ mod tests {
(config, temp_dir)
}
fn new_node() -> (Instance, TempDir) {
fn new_node() -> (GethNode, TempDir) {
let (args, temp_dir) = test_config();
let mut node = Instance::new(&args);
let mut node = GethNode::new(&args);
node.init(GENESIS_JSON.to_owned())
.expect("Failed to initialize the node")
.spawn_process()
@@ -555,110 +626,110 @@ mod tests {
#[test]
fn init_works() {
Instance::new(&test_config().0)
GethNode::new(&test_config().0)
.init(GENESIS_JSON.to_string())
.unwrap();
}
#[test]
fn spawn_works() {
Instance::new(&test_config().0)
GethNode::new(&test_config().0)
.spawn(GENESIS_JSON.to_string())
.unwrap();
}
#[test]
fn version_works() {
let version = Instance::new(&test_config().0).version().unwrap();
let version = GethNode::new(&test_config().0).version().unwrap();
assert!(
version.starts_with("geth version"),
"expected version string, got: '{version}'"
);
}
#[test]
fn can_get_chain_id_from_node() {
#[tokio::test]
async fn can_get_chain_id_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
// Act
let chain_id = node.chain_id();
let chain_id = node.chain_id().await;
// Assert
let chain_id = chain_id.expect("Failed to get the chain id");
assert_eq!(chain_id, 420_420_420);
}
#[test]
fn can_get_gas_limit_from_node() {
#[tokio::test]
async fn can_get_gas_limit_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
// Act
let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest);
let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest).await;
// Assert
let gas_limit = gas_limit.expect("Failed to get the gas limit");
assert_eq!(gas_limit, u32::MAX as u128)
}
#[test]
fn can_get_coinbase_from_node() {
#[tokio::test]
async fn can_get_coinbase_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
// Act
let coinbase = node.block_coinbase(BlockNumberOrTag::Latest);
let coinbase = node.block_coinbase(BlockNumberOrTag::Latest).await;
// Assert
let coinbase = coinbase.expect("Failed to get the coinbase");
assert_eq!(coinbase, Address::new([0xFF; 20]))
}
#[test]
fn can_get_block_difficulty_from_node() {
#[tokio::test]
async fn can_get_block_difficulty_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
// Act
let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest);
let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest).await;
// Assert
let block_difficulty = block_difficulty.expect("Failed to get the block difficulty");
assert_eq!(block_difficulty, U256::ZERO)
}
#[test]
fn can_get_block_hash_from_node() {
#[tokio::test]
async fn can_get_block_hash_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
// Act
let block_hash = node.block_hash(BlockNumberOrTag::Latest);
let block_hash = node.block_hash(BlockNumberOrTag::Latest).await;
// Assert
let _ = block_hash.expect("Failed to get the block hash");
}
#[test]
fn can_get_block_timestamp_from_node() {
#[tokio::test]
async fn can_get_block_timestamp_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
// Act
let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest);
let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest).await;
// Assert
let _ = block_timestamp.expect("Failed to get the block timestamp");
}
#[test]
fn can_get_block_number_from_node() {
#[tokio::test]
async fn can_get_block_number_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
// Act
let block_number = node.last_block_number();
let block_number = node.last_block_number().await;
// Assert
let block_number = block_number.expect("Failed to get the block number");
+157 -119
View File
@@ -16,7 +16,8 @@ use alloy::{
TransactionBuilderError, UnbuiltTransactionError,
},
primitives::{
Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, FixedBytes, U256,
Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, FixedBytes,
StorageKey, TxHash, U256,
},
providers::{
Provider, ProviderBuilder,
@@ -24,12 +25,15 @@ use alloy::{
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
},
rpc::types::{
TransactionReceipt,
EIP1186AccountProofResponse, TransactionReceipt,
eth::{Block, Header, Transaction},
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
},
signers::local::PrivateKeySigner,
};
use anyhow::Context;
use revive_common::EVMVersion;
use revive_dt_common::fs::clear_directory;
use revive_dt_format::traits::ResolverApi;
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
@@ -37,7 +41,6 @@ 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;
@@ -86,6 +89,9 @@ impl KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn init(&mut self, genesis: &str) -> anyhow::Result<&mut Self> {
let _ = clear_directory(&self.base_directory);
let _ = clear_directory(&self.logs_directory);
create_dir_all(&self.base_directory)?;
create_dir_all(&self.logs_directory)?;
@@ -154,7 +160,7 @@ impl KitchensinkNode {
Ok(self)
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
fn spawn_process(&mut self) -> anyhow::Result<()> {
let substrate_rpc_port = Self::BASE_SUBSTRATE_RPC_PORT + self.id as u16;
let proxy_rpc_port = Self::BASE_PROXY_RPC_PORT + self.id as u16;
@@ -206,7 +212,7 @@ impl KitchensinkNode {
if let Err(error) = Self::wait_ready(
self.kitchensink_stderr_log_file_path().as_path(),
Self::SUBSTRATE_READY_MARKER,
Duration::from_secs(30),
Duration::from_secs(60),
) {
tracing::error!(
?error,
@@ -235,7 +241,7 @@ impl KitchensinkNode {
if let Err(error) = Self::wait_ready(
self.proxy_stderr_log_file_path().as_path(),
Self::ETH_PROXY_READY_MARKER,
Duration::from_secs(30),
Duration::from_secs(60),
) {
tracing::error!(?error, "Failed to start proxy, shutting down gracefully");
self.shutdown()?;
@@ -252,7 +258,7 @@ impl KitchensinkNode {
Ok(())
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
fn extract_balance_from_genesis_file(
&self,
genesis: &Genesis,
@@ -301,7 +307,7 @@ impl KitchensinkNode {
}
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
pub fn eth_rpc_version(&self) -> anyhow::Result<String> {
let output = Command::new(&self.eth_proxy_binary)
.arg("--version")
@@ -361,9 +367,9 @@ impl KitchensinkNode {
.disable_recommended_fillers()
.network::<KitchenSinkNetwork>()
.filler(FallbackGasFiller::new(
30_000_000,
200_000_000_000,
3_000_000_000,
25_000_000,
1_000_000_000,
1_000_000_000,
))
.filler(ChainIdFiller::default())
.filler(NonceFiller::new(nonce_manager))
@@ -376,138 +382,170 @@ impl KitchensinkNode {
}
impl EthereumNode for KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn execute_transaction(
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn execute_transaction(
&self,
transaction: alloy::rpc::types::TransactionRequest,
) -> anyhow::Result<TransactionReceipt> {
tracing::debug!(?transaction, "Submitting transaction");
let provider = self.provider();
let receipt = BlockingExecutor::execute(async move {
Ok(provider
let receipt = self
.provider()
.await?
.send_transaction(transaction)
.await?
.get_receipt()
.await?)
})?;
.await?;
tracing::info!(?receipt, "Submitted tx to kitchensink");
receipt
Ok(receipt)
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn trace_transaction(
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn trace_transaction(
&self,
transaction: &TransactionReceipt,
trace_options: GethDebugTracingOptions,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
let tx_hash = transaction.transaction_hash;
let provider = self.provider();
BlockingExecutor::execute(async move {
Ok(provider
Ok(self
.provider()
.await?
.debug_trace_transaction(tx_hash, trace_options)
.await?)
})?
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
match self
.trace_transaction(transaction, trace_options)?
.trace_transaction(transaction, trace_options)
.await?
.try_into_pre_state_frame()?
{
PreStateFrame::Diff(diff) => Ok(diff),
_ => anyhow::bail!("expected a diff mode trace"),
}
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn balance_of(&self, address: Address) -> anyhow::Result<U256> {
self.provider()
.await?
.get_balance(address)
.await
.map_err(Into::into)
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn latest_state_proof(
&self,
address: Address,
keys: Vec<StorageKey>,
) -> anyhow::Result<EIP1186AccountProofResponse> {
self.provider()
.await?
.get_proof(address, keys)
.latest()
.await
.map_err(Into::into)
}
}
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();
BlockingExecutor::execute(async move {
provider.await?.get_chain_id().await.map_err(Into::into)
})?
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
self.provider()
.await?
.get_chain_id()
.await
.map_err(Into::into)
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn transaction_gas_price(&self, tx_hash: &TxHash) -> anyhow::Result<u128> {
self.provider()
.await?
.get_transaction_receipt(*tx_hash)
.await?
.context("Failed to get the transaction receipt")
.map(|receipt| receipt.effective_gas_price)
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.gas_limit)
})?
.map(|block| block.header.gas_limit as _)
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.beneficiary)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.difficulty)
})?
.map(|block| U256::from_be_bytes(block.header.mix_hash.0))
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn block_base_fee(&self, number: BlockNumberOrTag) -> anyhow::Result<u64> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.and_then(|block| {
block
.header
.base_fee_per_gas
.context("Failed to get the base fee per gas")
})
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.hash)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> {
self.provider()
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.timestamp)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider.await?.get_block_number().await.map_err(Into::into)
})?
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
async fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
self.provider()
.await?
.get_block_number()
.await
.map_err(Into::into)
}
}
@@ -549,7 +587,7 @@ impl Node for KitchensinkNode {
self.rpc_url.clone()
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
fn shutdown(&mut self) -> anyhow::Result<()> {
// Terminate the processes in a graceful manner to allow for the output to be flushed.
if let Some(mut child) = self.process_proxy.take() {
@@ -576,12 +614,12 @@ impl Node for KitchensinkNode {
Ok(())
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
fn spawn(&mut self, genesis: String) -> anyhow::Result<()> {
self.init(&genesis)?.spawn_process()
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id), err)]
fn version(&self) -> anyhow::Result<String> {
let output = Command::new(&self.substrate_binary)
.arg("--version")
@@ -601,6 +639,10 @@ impl Node for KitchensinkNode {
Some(targets) => targets.iter().any(|str| str.as_str() == "pvm"),
}
}
fn evm_version() -> EVMVersion {
EVMVersion::Cancun
}
}
impl Drop for KitchensinkNode {
@@ -1043,26 +1085,21 @@ mod tests {
use revive_dt_config::Arguments;
use std::path::PathBuf;
use std::sync::{LazyLock, Mutex};
use temp_dir::TempDir;
use std::fs;
use super::*;
use crate::{GENESIS_JSON, Node};
fn test_config() -> (Arguments, TempDir) {
let mut config = Arguments::default();
let temp_dir = TempDir::new().unwrap();
config.working_directory = temp_dir.path().to_path_buf().into();
config.kitchensink = PathBuf::from("substrate-node");
config.eth_proxy = PathBuf::from("eth-rpc");
(config, temp_dir)
fn test_config() -> Arguments {
Arguments {
kitchensink: PathBuf::from("substrate-node"),
eth_proxy: PathBuf::from("eth-rpc"),
..Default::default()
}
}
fn new_node() -> (KitchensinkNode, Arguments, TempDir) {
fn new_node() -> (KitchensinkNode, Arguments) {
// Note: When we run the tests in the CI we found that if they're all
// run in parallel then the CI is unable to start all of the nodes in
// time and their start up times-out. Therefore, we want all of the
@@ -1081,20 +1118,20 @@ mod tests {
static NODE_START_MUTEX: Mutex<()> = Mutex::new(());
let _guard = NODE_START_MUTEX.lock().unwrap();
let (args, temp_dir) = test_config();
let args = test_config();
let mut node = KitchensinkNode::new(&args);
node.init(GENESIS_JSON)
.expect("Failed to initialize the node")
.spawn_process()
.expect("Failed to spawn the node process");
(node, args, temp_dir)
(node, args)
}
/// A shared node that multiple tests can use. It starts up once.
fn shared_node() -> &'static KitchensinkNode {
static NODE: LazyLock<(KitchensinkNode, TempDir)> = LazyLock::new(|| {
let (node, _, temp_dir) = new_node();
(node, temp_dir)
static NODE: LazyLock<(KitchensinkNode, Arguments)> = LazyLock::new(|| {
let (node, args) = new_node();
(node, args)
});
&NODE.0
}
@@ -1102,7 +1139,7 @@ mod tests {
#[tokio::test]
async fn node_mines_simple_transfer_transaction_and_returns_receipt() {
// Arrange
let (node, args, _temp_dir) = new_node();
let (node, args) = new_node();
let provider = node.provider().await.expect("Failed to create provider");
@@ -1137,7 +1174,7 @@ mod tests {
}
"#;
let mut dummy_node = KitchensinkNode::new(&test_config().0);
let mut dummy_node = KitchensinkNode::new(&test_config());
// Call `init()`
dummy_node.init(genesis_content).expect("init failed");
@@ -1181,7 +1218,7 @@ mod tests {
}
"#;
let node = KitchensinkNode::new(&test_config().0);
let node = KitchensinkNode::new(&test_config());
let result = node
.extract_balance_from_genesis_file(&serde_json::from_str(genesis_json).unwrap())
@@ -1252,15 +1289,16 @@ mod tests {
#[test]
fn spawn_works() {
let (config, _temp_dir) = test_config();
let config = test_config();
let mut node = KitchensinkNode::new(&config);
node.spawn(GENESIS_JSON.to_string()).unwrap();
}
#[test]
fn version_works() {
let (config, _temp_dir) = test_config();
let config = test_config();
let node = KitchensinkNode::new(&config);
let version = node.version().unwrap();
@@ -1273,7 +1311,7 @@ mod tests {
#[test]
fn eth_rpc_version_works() {
let (config, _temp_dir) = test_config();
let config = test_config();
let node = KitchensinkNode::new(&config);
let version = node.eth_rpc_version().unwrap();
@@ -1284,86 +1322,86 @@ mod tests {
);
}
#[test]
fn can_get_chain_id_from_node() {
#[tokio::test]
async fn can_get_chain_id_from_node() {
// Arrange
let node = shared_node();
// Act
let chain_id = node.chain_id();
let chain_id = node.chain_id().await;
// Assert
let chain_id = chain_id.expect("Failed to get the chain id");
assert_eq!(chain_id, 420_420_420);
}
#[test]
fn can_get_gas_limit_from_node() {
#[tokio::test]
async fn can_get_gas_limit_from_node() {
// Arrange
let node = shared_node();
// Act
let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest);
let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest).await;
// Assert
let _ = gas_limit.expect("Failed to get the gas limit");
}
#[test]
fn can_get_coinbase_from_node() {
#[tokio::test]
async fn can_get_coinbase_from_node() {
// Arrange
let node = shared_node();
// Act
let coinbase = node.block_coinbase(BlockNumberOrTag::Latest);
let coinbase = node.block_coinbase(BlockNumberOrTag::Latest).await;
// Assert
let _ = coinbase.expect("Failed to get the coinbase");
}
#[test]
fn can_get_block_difficulty_from_node() {
#[tokio::test]
async fn can_get_block_difficulty_from_node() {
// Arrange
let node = shared_node();
// Act
let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest);
let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest).await;
// Assert
let _ = block_difficulty.expect("Failed to get the block difficulty");
}
#[test]
fn can_get_block_hash_from_node() {
#[tokio::test]
async fn can_get_block_hash_from_node() {
// Arrange
let node = shared_node();
// Act
let block_hash = node.block_hash(BlockNumberOrTag::Latest);
let block_hash = node.block_hash(BlockNumberOrTag::Latest).await;
// Assert
let _ = block_hash.expect("Failed to get the block hash");
}
#[test]
fn can_get_block_timestamp_from_node() {
#[tokio::test]
async fn can_get_block_timestamp_from_node() {
// Arrange
let node = shared_node();
// Act
let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest);
let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest).await;
// Assert
let _ = block_timestamp.expect("Failed to get the block timestamp");
}
#[test]
fn can_get_block_number_from_node() {
#[tokio::test]
async fn can_get_block_number_from_node() {
// Arrange
let node = shared_node();
// Act
let block_number = node.last_block_number();
let block_number = node.last_block_number().await;
// Assert
let _ = block_number.expect("Failed to get the block number");
+4
View File
@@ -1,5 +1,6 @@
//! This crate implements the testing nodes.
use revive_common::EVMVersion;
use revive_dt_config::Arguments;
use revive_dt_node_interaction::EthereumNode;
@@ -36,4 +37,7 @@ pub trait Node: EthereumNode {
/// Given a list of targets from the metadata file, this function determines if the metadata
/// file can be ran on this node or not.
fn matches_target(&self, targets: Option<&[String]>) -> bool;
/// Returns the EVM version of the node.
fn evm_version() -> EVMVersion;
}
+3 -2
View File
@@ -1,11 +1,12 @@
//! This crate implements concurrent handling of testing node.
use std::{
fs::read_to_string,
sync::atomic::{AtomicUsize, Ordering},
thread,
};
use revive_dt_common::cached_fs::read_to_string;
use anyhow::Context;
use revive_dt_config::Arguments;
@@ -24,7 +25,7 @@ where
{
/// Create a new Pool. This will start as many nodes as there are workers in `config`.
pub fn new(config: &Arguments) -> anyhow::Result<Self> {
let nodes = config.workers;
let nodes = config.number_of_nodes;
let genesis = read_to_string(&config.genesis_file).context(format!(
"can not read genesis file: {}",
config.genesis_file.display()
+2 -1
View File
@@ -8,11 +8,12 @@ repository.workspace = true
rust-version.workspace = true
[dependencies]
revive-dt-common = { workspace = true }
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 }
+8 -21
View File
@@ -1,5 +1,6 @@
//! The report analyzer enriches the raw report data.
use revive_dt_compiler::CompilerOutput;
use serde::{Deserialize, Serialize};
use crate::reporter::CompilationTask;
@@ -13,41 +14,27 @@ 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 containes a lot of comments.
/// Is a proxy because the YUL also contains 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(output) = &compilation_task.json_output else {
return;
};
let Some(contracts) = &output.contracts else {
let Some(CompilerOutput { contracts }) = &compilation_task.json_output else {
return;
};
for (_solidity, contracts) 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;
};
for (_name, (bytecode, _)) in contracts.iter() {
// 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 = deploy_code.object.len() / 2;
let bytecode_size = bytecode.len() / 2;
let yul_size = contract
.ir_optimized
.as_ref()
.expect("if the contract has a deploy code it should also have the opimized IR")
.len();
// TODO: for the time being we set the yul_size to be zero. We need to change this
// when we overhaul the reporting.
self.update_sizes(bytecode_size, yul_size);
self.update_sizes(bytecode_size, 0);
}
}
}
+12 -19
View File
@@ -12,18 +12,19 @@ use std::{
};
use anyhow::Context;
use serde::{Deserialize, Serialize};
use serde::Serialize;
use revive_dt_common::types::Mode;
use revive_dt_compiler::{CompilerInput, CompilerOutput};
use revive_dt_config::{Arguments, TestingPlatform};
use revive_dt_format::{corpus::Corpus, mode::SolcMode};
use revive_solc_json_interface::{SolcStandardJsonInput, SolcStandardJsonOutput};
use revive_dt_format::corpus::Corpus;
use crate::analyzer::CompilerStatistics;
pub(crate) static REPORTER: OnceLock<Mutex<Report>> = OnceLock::new();
/// The `Report` datastructure stores all relevant inforamtion required for generating reports.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, Serialize)]
pub struct Report {
/// The configuration used during the test.
pub config: Arguments,
@@ -41,14 +42,14 @@ pub struct Report {
}
/// Contains a compiled contract.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize)]
pub struct CompilationTask {
/// The observed compiler input.
pub json_input: SolcStandardJsonInput,
pub json_input: CompilerInput,
/// The observed compiler output.
pub json_output: Option<SolcStandardJsonOutput>,
pub json_output: Option<CompilerOutput>,
/// The observed compiler mode.
pub mode: SolcMode,
pub mode: Mode,
/// The observed compiler version.
pub compiler_version: String,
/// The observed error, if any.
@@ -56,7 +57,7 @@ pub struct CompilationTask {
}
/// Represents a report about a compilation task.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize)]
pub struct CompilationResult {
/// The observed compilation task.
pub compilation_task: CompilationTask,
@@ -65,7 +66,7 @@ pub struct CompilationResult {
}
/// The [Span] struct indicates the context of what is being reported.
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[derive(Clone, Copy, Debug, Serialize)]
pub struct Span {
/// The corpus index this belongs to.
corpus: usize,
@@ -152,15 +153,7 @@ 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()
&& result
.compilation_task
.json_output
.as_ref()
.and_then(|output| output.errors.as_ref())
.map(|errors| errors.is_empty())
.unwrap_or(true)
{
if result.compilation_task.error.is_none() {
continue;
}
+3
View File
@@ -9,9 +9,12 @@ repository.workspace = true
rust-version.workspace = true
[dependencies]
revive-dt-common = { workspace = true }
anyhow = { workspace = true }
hex = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true }
reqwest = { workspace = true }
semver = { workspace = true }
serde = { workspace = true }
+10 -8
View File
@@ -6,37 +6,39 @@ use std::{
io::{BufWriter, Write},
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
sync::{LazyLock, Mutex},
sync::LazyLock,
};
use crate::download::GHDownloader;
use tokio::sync::Mutex;
use crate::download::SolcDownloader;
pub const SOLC_CACHE_DIRECTORY: &str = "solc";
pub(crate) static SOLC_CACHER: LazyLock<Mutex<HashSet<PathBuf>>> = LazyLock::new(Default::default);
pub(crate) fn get_or_download(
pub(crate) async fn get_or_download(
working_directory: &Path,
downloader: &GHDownloader,
downloader: &SolcDownloader,
) -> anyhow::Result<PathBuf> {
let target_directory = working_directory
.join(SOLC_CACHE_DIRECTORY)
.join(downloader.version.to_string());
let target_file = target_directory.join(downloader.target);
let mut cache = SOLC_CACHER.lock().unwrap();
let mut cache = SOLC_CACHER.lock().await;
if cache.contains(&target_file) {
tracing::debug!("using cached solc: {}", target_file.display());
return Ok(target_file);
}
create_dir_all(target_directory)?;
download_to_file(&target_file, downloader)?;
download_to_file(&target_file, downloader).await?;
cache.insert(target_file.clone());
Ok(target_file)
}
fn download_to_file(path: &Path, downloader: &GHDownloader) -> anyhow::Result<()> {
async fn download_to_file(path: &Path, downloader: &SolcDownloader) -> anyhow::Result<()> {
tracing::info!("caching file: {}", path.display());
let Ok(file) = File::create_new(path) else {
@@ -52,7 +54,7 @@ fn download_to_file(path: &Path, downloader: &GHDownloader) -> anyhow::Result<()
}
let mut file = BufWriter::new(file);
file.write_all(&downloader.download()?)?;
file.write_all(&downloader.download().await?)?;
file.flush()?;
drop(file);
+105 -48
View File
@@ -5,6 +5,8 @@ use std::{
sync::{LazyLock, Mutex},
};
use revive_dt_common::types::VersionOrRequirement;
use semver::Version;
use sha2::{Digest, Sha256};
@@ -23,12 +25,12 @@ impl List {
///
/// Caches the list retrieved from the `url` into [LIST_CACHE],
/// subsequent calls with the same `url` will return the cached list.
pub fn download(url: &'static str) -> anyhow::Result<Self> {
pub async fn download(url: &'static str) -> anyhow::Result<Self> {
if let Some(list) = LIST_CACHE.lock().unwrap().get(url) {
return Ok(list.clone());
}
let body: List = reqwest::blocking::get(url)?.json()?;
let body: List = reqwest::get(url).await?.json().await?;
LIST_CACHE.lock().unwrap().insert(url, body.clone());
@@ -36,65 +38,91 @@ impl List {
}
}
/// Download solc binaries from GitHub releases (IPFS links aren't reliable).
/// Download solc binaries from the official SolidityLang site
#[derive(Clone, Debug)]
pub struct GHDownloader {
pub struct SolcDownloader {
pub version: Version,
pub target: &'static str,
pub list: &'static str,
}
impl GHDownloader {
pub const BASE_URL: &str = "https://github.com/ethereum/solidity/releases/download";
impl SolcDownloader {
pub const BASE_URL: &str = "https://binaries.soliditylang.org";
pub const LINUX_NAME: &str = "solc-static-linux";
pub const MACOSX_NAME: &str = "solc-macos";
pub const WINDOWS_NAME: &str = "solc-windows.exe";
pub const WASM_NAME: &str = "soljson.js";
pub const LINUX_NAME: &str = "linux-amd64";
pub const MACOSX_NAME: &str = "macosx-amd64";
pub const WINDOWS_NAME: &str = "windows-amd64";
pub const WASM_NAME: &str = "wasm";
fn new(version: Version, target: &'static str, list: &'static str) -> Self {
Self {
async 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)
.await?
.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,
})
}
}
}
pub fn linux(version: Version) -> Self {
Self::new(version, Self::LINUX_NAME, List::LINUX_URL)
pub async fn linux(version: impl Into<VersionOrRequirement>) -> anyhow::Result<Self> {
Self::new(version, Self::LINUX_NAME, List::LINUX_URL).await
}
pub fn macosx(version: Version) -> Self {
Self::new(version, Self::MACOSX_NAME, List::MACOSX_URL)
pub async fn macosx(version: impl Into<VersionOrRequirement>) -> anyhow::Result<Self> {
Self::new(version, Self::MACOSX_NAME, List::MACOSX_URL).await
}
pub fn windows(version: Version) -> Self {
Self::new(version, Self::WINDOWS_NAME, List::WINDOWS_URL)
pub async fn windows(version: impl Into<VersionOrRequirement>) -> anyhow::Result<Self> {
Self::new(version, Self::WINDOWS_NAME, List::WINDOWS_URL).await
}
pub fn wasm(version: Version) -> Self {
Self::new(version, Self::WASM_NAME, List::WASM_URL)
}
/// Returns the download link.
pub fn url(&self) -> String {
format!("{}/v{}/{}", Self::BASE_URL, &self.version, &self.target)
pub async fn wasm(version: impl Into<VersionOrRequirement>) -> anyhow::Result<Self> {
Self::new(version, Self::WASM_NAME, List::WASM_URL).await
}
/// Download the solc binary.
///
/// Errors out if the download fails or the digest of the downloaded file
/// mismatches the expected digest from the release [List].
pub fn download(&self) -> anyhow::Result<Vec<u8>> {
pub async fn download(&self) -> anyhow::Result<Vec<u8>> {
tracing::info!("downloading solc: {self:?}");
let expected_digest = List::download(self.list)?
.builds
let builds = List::download(self.list).await?.builds;
let build = builds
.iter()
.find(|build| build.version == self.version)
.ok_or_else(|| anyhow::anyhow!("solc v{} not found builds", self.version))
.map(|b| b.sha256.strip_prefix("0x").unwrap_or(&b.sha256).to_string())?;
.ok_or_else(|| anyhow::anyhow!("solc v{} not found builds", self.version))?;
let file = reqwest::blocking::get(self.url())?.bytes()?.to_vec();
let path = build.path.clone();
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).await?.bytes().await?.to_vec();
if hex::encode(Sha256::digest(&file)) != expected_digest {
anyhow::bail!("sha256 mismatch for solc version {}", self.version);
@@ -106,29 +134,58 @@ impl GHDownloader {
#[cfg(test)]
mod tests {
use crate::{download::GHDownloader, list::List};
use crate::{download::SolcDownloader, list::List};
#[test]
fn try_get_windows() {
let version = List::download(List::WINDOWS_URL).unwrap().latest_release;
GHDownloader::windows(version).download().unwrap();
#[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();
}
#[test]
fn try_get_macosx() {
let version = List::download(List::MACOSX_URL).unwrap().latest_release;
GHDownloader::macosx(version).download().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();
}
#[test]
fn try_get_linux() {
let version = List::download(List::LINUX_URL).unwrap().latest_release;
GHDownloader::linux(version).download().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();
}
#[test]
fn try_get_wasm() {
let version = List::download(List::WASM_URL).unwrap().latest_release;
GHDownloader::wasm(version).download().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();
}
}
+11 -10
View File
@@ -6,8 +6,9 @@
use std::path::{Path, PathBuf};
use cache::get_or_download;
use download::GHDownloader;
use semver::Version;
use download::SolcDownloader;
use revive_dt_common::types::VersionOrRequirement;
pub mod cache;
pub mod download;
@@ -18,22 +19,22 @@ pub mod list;
///
/// Subsequent calls for the same version will use a cached artifact
/// and not download it again.
pub fn download_solc(
pub async fn download_solc(
cache_directory: &Path,
version: Version,
version: impl Into<VersionOrRequirement>,
wasm: bool,
) -> anyhow::Result<PathBuf> {
let downloader = if wasm {
GHDownloader::wasm(version)
SolcDownloader::wasm(version).await
} else if cfg!(target_os = "linux") {
GHDownloader::linux(version)
SolcDownloader::linux(version).await
} else if cfg!(target_os = "macos") {
GHDownloader::macosx(version)
SolcDownloader::macosx(version).await
} else if cfg!(target_os = "windows") {
GHDownloader::windows(version)
SolcDownloader::windows(version).await
} else {
unimplemented!()
};
}?;
get_or_download(cache_directory, &downloader)
get_or_download(cache_directory, &downloader).await
}