diff --git a/Cargo.lock b/Cargo.lock index a8c93a4..cec5aed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2873,6 +2873,7 @@ dependencies = [ name = "revive-dt-core" version = "0.1.0" dependencies = [ + "alloy", "anyhow", "clap", "env_logger", @@ -2882,6 +2883,7 @@ dependencies = [ "revive-dt-config", "revive-dt-format", "revive-dt-node", + "revive-dt-node-interaction", "revive-solc-json-interface", "semver 1.0.26", "serde", diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 30a346b..bb8934c 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -3,7 +3,7 @@ //! - Polkadot revive resolc compiler //! - Polkadot revive Wasm compiler -use std::{fs::read_to_string, path::Path}; +use std::{fs::read_to_string, hash::Hash, path::Path}; use revive_common::EVMVersion; use revive_solc_json_interface::{ @@ -20,21 +20,57 @@ pub mod solc; /// A common interface for all supported Solidity compilers. pub trait SolidityCompiler { /// Extra options specific to the compiler. - type Options; + type Options: Default + PartialEq + Eq + Hash; + /// The low-level compiler interface. fn build( &self, - input: &SolcStandardJsonInput, - extra_options: &Option, - ) -> anyhow::Result; + input: CompilerInput, + ) -> anyhow::Result>; fn new(solc_version: &Version) -> Self; } +/// The generic compilation input configuration. +#[derive(Debug)] +pub struct CompilerInput { + pub extra_options: T, + pub input: SolcStandardJsonInput, +} + +/// The generic compilation output configuration. +pub struct CompilerOutput { + pub input: CompilerInput, + pub output: SolcStandardJsonOutput, +} + +impl PartialEq for CompilerInput +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 Eq for CompilerInput where T: PartialEq + Eq + Hash {} + +impl Hash for CompilerInput +where + T: PartialEq + Eq + Hash, +{ + fn hash(&self, state: &mut H) { + self.extra_options.hash(state); + state.write(&serde_json::to_vec(&self.input).unwrap_or_default()); + } +} + /// A generic builder style interface for configuring all compiler options. pub struct Compiler { input: SolcStandardJsonInput, - extra_options: Option, + extra_options: T::Options, allow_paths: Vec, base_path: Option, } @@ -68,7 +104,7 @@ where None, ), }, - extra_options: None, + extra_options: Default::default(), allow_paths: Default::default(), base_path: None, } @@ -92,7 +128,7 @@ where } pub fn extra_options(mut self, extra_options: T::Options) -> Self { - self.extra_options = Some(extra_options); + self.extra_options = extra_options; self } @@ -106,7 +142,10 @@ where self } - pub fn try_build(&self, solc_version: &Version) -> anyhow::Result { - T::new(solc_version).build(&self.input, &self.extra_options) + pub fn try_build(self, solc_version: &Version) -> anyhow::Result> { + T::new(solc_version).build(CompilerInput { + extra_options: self.extra_options, + input: self.input, + }) } } diff --git a/crates/compiler/src/solc.rs b/crates/compiler/src/solc.rs index 60bd9e9..f715d29 100644 --- a/crates/compiler/src/solc.rs +++ b/crates/compiler/src/solc.rs @@ -6,10 +6,9 @@ use std::{ process::{Command, Stdio}, }; -use revive_solc_json_interface::{SolcStandardJsonInput, SolcStandardJsonOutput}; use semver::Version; -use crate::SolidityCompiler; +use crate::{CompilerInput, CompilerOutput, SolidityCompiler}; pub struct Solc { binary_path: PathBuf, @@ -20,9 +19,8 @@ impl SolidityCompiler for Solc { fn build( &self, - input: &SolcStandardJsonInput, - _extra_options: &Option, - ) -> anyhow::Result { + input: CompilerInput, + ) -> anyhow::Result> { let mut child = Command::new(&self.binary_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -31,10 +29,13 @@ impl SolidityCompiler for Solc { .spawn()?; let stdin = child.stdin.as_mut().expect("should be piped"); - serde_json::to_writer(stdin, input)?; + serde_json::to_writer(stdin, &input.input)?; let output = child.wait_with_output()?.stdout; - Ok(serde_json::from_slice(&output)?) + Ok(CompilerOutput { + input, + output: serde_json::from_slice(&output)?, + }) } fn new(_solc_version: &Version) -> Self { diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 6bb15d2..9c9e65f 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; -use clap::Parser; +use clap::{Parser, ValueEnum}; #[derive(Debug, Parser, Clone)] #[command(name = "retester")] @@ -48,6 +48,18 @@ pub struct Arguments { default_value = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" )] pub account: String, + + /// The differential testing leader node implementation. + #[arg(short, long = "leader", default_value = "geth")] + pub leader: TestingPlatform, + + /// The differential testing follower node implementation. + #[arg(short, long = "follower", default_value = "kitchensink")] + pub follower: TestingPlatform, + + /// Only compile against this testing platform (doesn't execute the tests). + #[arg(short, long = "compile-only")] + pub compile_only: bool, } impl Default for Arguments { @@ -55,3 +67,15 @@ impl Default for Arguments { Arguments::parse_from(["retester"]) } } + +/// The Solidity compatible node implementation. +/// +/// This describes the solutions to be tested against on a high level. +#[derive(Clone, Debug, Eq, Hash, PartialEq, ValueEnum)] +#[clap(rename_all = "lower")] +pub enum TestingPlatform { + /// The go-ethereum reference full node EVM implementation. + Geth, + /// The kitchensink runtime provides the PolkaVM (PVM) based node implentation. + Kitchensink, +} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index a75ed83..f256d20 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -18,7 +18,9 @@ revive-dt-config = { workspace = true } revive-dt-format = { workspace = true } revive-solc-json-interface = { workspace = true } revive-dt-node = { workspace = true } +revive-dt-node-interaction = { workspace = true } +alloy = { workspace = true } anyhow = { workspace = true } clap = { workspace = true } log = { workspace = true } diff --git a/crates/core/src/driver/compiler.rs b/crates/core/src/driver/compiler.rs index f56428f..3e6d05a 100644 --- a/crates/core/src/driver/compiler.rs +++ b/crates/core/src/driver/compiler.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; -use revive_dt_compiler::{Compiler, solc::Solc}; +use revive_dt_compiler::{Compiler, CompilerInput, SolidityCompiler}; use revive_dt_format::{ metadata::Metadata, mode::{Mode, SolcMode}, @@ -10,15 +10,9 @@ use revive_dt_format::{ use revive_solc_json_interface::SolcStandardJsonOutput; use semver::Version; -#[derive(Hash, Eq, PartialEq)] -pub struct SolcSettings { - pub optimizer: bool, - pub solc_version: Version, -} - -pub fn build_evm( +pub fn build( metadata: &Metadata, -) -> anyhow::Result> { +) -> anyhow::Result, SolcStandardJsonOutput>> { let sources = metadata.contract_sources()?; let base_path = metadata.directory()?.display().to_string(); let modes = metadata @@ -28,7 +22,7 @@ pub fn build_evm( let mut result = HashMap::new(); for mode in modes { - let mut compiler = Compiler::::new().base_path(base_path.clone()); + let mut compiler = Compiler::::new().base_path(base_path.clone()); for (file, _contract) in sources.values() { compiler = compiler.with_source(file)?; } @@ -41,15 +35,8 @@ pub fn build_evm( }) => { let optimizer = solc_optimize.unwrap_or(true); let version = Version::new(0, 8, 29); - let out = compiler.solc_optimizer(optimizer).try_build(&version)?; - - result.insert( - SolcSettings { - optimizer: true, - solc_version: version, - }, - out, - ); + let output = compiler.solc_optimizer(optimizer).try_build(&version)?; + result.insert(output.input, output.output); } Mode::Unknown(mode) => log::debug!("compiler: ignoring unknown mode '{mode}'"), } diff --git a/crates/core/src/driver/mod.rs b/crates/core/src/driver/mod.rs index dd1b3c9..9403334 100644 --- a/crates/core/src/driver/mod.rs +++ b/crates/core/src/driver/mod.rs @@ -1,4 +1,60 @@ //! The test driver handles the compilation and execution of the test cases. +use alloy::primitives::map::HashMap; +use compiler::build; +use revive_dt_compiler::{CompilerInput, SolidityCompiler}; +use revive_dt_config::Arguments; +use revive_dt_format::metadata::Metadata; +use revive_dt_node::Node; +use revive_solc_json_interface::SolcStandardJsonOutput; + +use crate::Platform; + pub mod compiler; pub mod input; + +type Contracts = HashMap< + CompilerInput<<::Compiler as SolidityCompiler>::Options>, + SolcStandardJsonOutput, +>; + +pub struct Driver<'a, Leader: Platform, Follower: Platform> { + metadata: &'a Metadata, + config: &'a Arguments, + + leader_contracts: Contracts, + leader_node: ::Blockchain, + + follower_contracts: Contracts, + follower_node: ::Blockchain, +} + +impl<'a, L, F> Driver<'a, L, F> +where + L: Platform + Default, + F: Platform + Default, +{ + pub fn new(metadata: &'a Metadata, config: &'a Arguments) -> Driver<'a, L, F> { + Self { + metadata, + config, + + leader_node: <::Blockchain as Node>::new(config), + leader_contracts: Default::default(), + + follower_node: <::Blockchain as Node>::new(config), + follower_contracts: Default::default(), + } + } + + pub fn execute(&mut self) -> anyhow::Result<()> { + self.leader_contracts = build::(self.metadata)?; + self.follower_contracts = build::(self.metadata)?; + + if self.config.compile_only { + return Ok(()); + } + + todo!() + } +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 4ffcd22..c231476 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -2,7 +2,32 @@ //! //! This crate defines the testing configuration and //! provides a helper utilty to execute tests. -//! -//! + +use revive_dt_compiler::{SolidityCompiler, solc}; +use revive_dt_node::{Node, geth}; pub mod driver; + +/// One platform can be tested differentially against another. +/// +/// For this we need a blockchain node implementation and a compiler. +pub trait Platform { + type Blockchain: Node; + type Compiler: SolidityCompiler; +} + +#[derive(Default)] +pub struct Geth; + +impl Platform for Geth { + type Blockchain = geth::Instance; + type Compiler = solc::Solc; +} + +#[derive(Default)] +pub struct Kitchensink; + +impl Platform for Kitchensink { + type Blockchain = geth::Instance; + type Compiler = solc::Solc; +} diff --git a/crates/core/src/main.rs b/crates/core/src/main.rs index 16a5010..0abf9e6 100644 --- a/crates/core/src/main.rs +++ b/crates/core/src/main.rs @@ -4,7 +4,7 @@ use clap::Parser; use rayon::prelude::*; use revive_dt_config::*; -use revive_dt_core::driver::compiler::build_evm; +use revive_dt_core::{Geth, Kitchensink, driver::Driver}; use revive_dt_format::corpus::Corpus; use temp_dir::TempDir; @@ -27,22 +27,28 @@ fn main() -> anyhow::Result<()> { log::info!("found {} tests", tests.len()); tests.par_iter().for_each(|metadata| { - let _ = match build_evm(metadata) { + let mut driver = match (&args.leader, &args.follower) { + (TestingPlatform::Geth, TestingPlatform::Kitchensink) => { + Driver::::new(metadata, &args) + } + _ => unimplemented!(), + }; + + match driver.execute() { Ok(build) => { log::info!( - "metadata {} compilation success", + "metadata {} success", metadata.file_path.as_ref().unwrap().display() ); build } Err(error) => { log::warn!( - "metadata {} compilation failure: {error:?}", + "metadata {} failure: {error:?}", metadata.file_path.as_ref().unwrap().display() ); - return; } - }; + } }); } diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index d4ff304..ca39303 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -4,12 +4,12 @@ use serde::{Deserialize, de::Deserializer}; #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] pub struct Input { - instance: String, + pub instance: String, #[serde(deserialize_with = "deserialize_method")] - method: Method, + pub method: Method, #[serde(deserialize_with = "deserialize_calldata")] - calldata: Vec, - expected: Option>, + pub calldata: Vec, + pub expected: Option>, } /// Specify how the contract is called. diff --git a/crates/node/src/geth.rs b/crates/node/src/geth.rs index 7000633..17be4c6 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -52,28 +52,6 @@ impl Instance { const READY_MARKER: &str = "IPC endpoint opened"; const ERROR_MARKER: &str = "Fatal:"; - /// Create a new uninitialized instance. - pub fn new(config: &Arguments) -> anyhow::Result { - let geth_directory = config - .working_directory - .as_ref() - .ok_or_else(|| anyhow::anyhow!("config did not provide working directory"))? - .join(Self::BASE_DIRECTORY); - let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst); - let base_directory = geth_directory.join(id.to_string()); - - Ok(Self { - connection_string: base_directory.join(Self::IPC_FILE).display().to_string(), - data_directory: base_directory.join(Self::DATA_DIRECTORY), - base_directory, - geth: config.geth.clone(), - id, - handle: None, - network_id: config.network_id, - start_timeout: config.geth_start_timeout, - }) - } - /// Create the node directory and call `geth init` to configure the genesis. fn init(&mut self, genesis: String) -> anyhow::Result<&mut Self> { let geth_directory = self.base_directory.parent().expect("the id should be set"); @@ -182,6 +160,27 @@ impl EthereumNode for Instance { } impl Node for Instance { + fn new(config: &Arguments) -> Self { + let geth_directory = config + .working_directory + .as_ref() + .expect("config should provide working directory") + .join(Self::BASE_DIRECTORY); + let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst); + let base_directory = geth_directory.join(id.to_string()); + + Self { + connection_string: base_directory.join(Self::IPC_FILE).display().to_string(), + data_directory: base_directory.join(Self::DATA_DIRECTORY), + base_directory, + geth: config.geth.clone(), + id, + handle: None, + network_id: config.network_id, + start_timeout: config.geth_start_timeout, + } + } + fn connection_string(&self) -> String { self.connection_string.clone() } @@ -252,7 +251,6 @@ mod tests { #[test] fn init_works() { Instance::new(&test_config().0) - .unwrap() .init(GENESIS_JSON.to_string()) .unwrap(); } @@ -260,14 +258,13 @@ mod tests { #[test] fn spawn_works() { Instance::new(&test_config().0) - .unwrap() .spawn(GENESIS_JSON.to_string()) .unwrap(); } #[test] fn version_works() { - let version = Instance::new(&test_config().0).unwrap().version().unwrap(); + let version = Instance::new(&test_config().0).version().unwrap(); assert!( version.starts_with("geth version"), "expected version string, got: '{version}'" diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index a98e995..3b3cd39 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -1,6 +1,7 @@ //! This crate implements the testing nodes. use alloy::rpc::types::{TransactionReceipt, trace::geth::DiffMode}; +use revive_dt_config::Arguments; use revive_dt_node_interaction::EthereumNode; pub mod geth; @@ -10,6 +11,9 @@ pub const GENESIS_JSON: &str = include_str!("../../../genesis.json"); /// An abstract interface for testing nodes. pub trait Node: EthereumNode { + /// Create a new uninitialized instance. + fn new(config: &Arguments) -> Self; + /// Spawns a node configured according to the genesis json. /// /// Blocking until it's ready to accept transactions.