//! The global configuration used across all revive differential testing crates. use std::{ fmt::Display, fs::read_to_string, ops::Deref, path::{Path, PathBuf}, str::FromStr, sync::{Arc, LazyLock, OnceLock}, time::Duration, }; use alloy::{ genesis::Genesis, hex::ToHexExt, network::EthereumWallet, primitives::{FixedBytes, U256}, signers::local::PrivateKeySigner, }; use clap::{Parser, ValueEnum, ValueHint}; use revive_dt_common::types::PlatformIdentifier; use semver::Version; use serde::{Serialize, Serializer}; use strum::{AsRefStr, Display, EnumString, IntoStaticStr}; use temp_dir::TempDir; #[derive(Clone, Debug, Parser, Serialize)] #[command(name = "retester")] pub enum Context { /// Executes tests in the MatterLabs format differentially on multiple targets concurrently. ExecuteTests(Box), /// Exports the JSON schema of the MatterLabs test format used by the tool. ExportJsonSchema, } impl Context { pub fn working_directory_configuration(&self) -> &WorkingDirectoryConfiguration { self.as_ref() } pub fn report_configuration(&self) -> &ReportConfiguration { self.as_ref() } } impl AsRef for Context { fn as_ref(&self) -> &WorkingDirectoryConfiguration { match self { Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } } impl AsRef for Context { fn as_ref(&self) -> &SolcConfiguration { match self { Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } } impl AsRef for Context { fn as_ref(&self) -> &ResolcConfiguration { match self { Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } } impl AsRef for Context { fn as_ref(&self) -> &GethConfiguration { match self { Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } } impl AsRef for Context { fn as_ref(&self) -> &KurtosisConfiguration { match self { Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } } impl AsRef for Context { fn as_ref(&self) -> &ZombieNetConfiguration { match self { Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } } impl AsRef for Context { fn as_ref(&self) -> &KitchensinkConfiguration { match self { Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } } impl AsRef for Context { fn as_ref(&self) -> &ReviveDevNodeConfiguration { match self { Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } } impl AsRef for Context { fn as_ref(&self) -> &EthRpcConfiguration { match self { Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } } impl AsRef for Context { fn as_ref(&self) -> &GenesisConfiguration { match self { Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } } impl AsRef for Context { fn as_ref(&self) -> &WalletConfiguration { match self { Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } } impl AsRef for Context { fn as_ref(&self) -> &ConcurrencyConfiguration { match self { Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } } impl AsRef for Context { fn as_ref(&self) -> &CompilationConfiguration { match self { Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } } impl AsRef for Context { fn as_ref(&self) -> &ReportConfiguration { match self { Self::ExecuteTests(context) => context.as_ref().as_ref(), Self::ExportJsonSchema => unreachable!(), } } } #[derive(Clone, Debug, Parser, Serialize)] pub struct TestExecutionContext { /// The working directory that the program will use for all of the temporary artifacts needed at /// runtime. /// /// If not specified, then a temporary directory will be created and used by the program for all /// temporary artifacts. #[clap( short, long, default_value = "", value_hint = ValueHint::DirPath, )] pub working_directory: WorkingDirectoryConfiguration, /// The set of platforms that the differential tests should run on. #[arg( short = 'p', long = "platform", default_values = ["geth-evm-solc", "revive-dev-node-polkavm-resolc"] )] pub platforms: Vec, /// A list of test corpus JSON files to be tested. #[arg(long = "corpus", short)] pub corpus: Vec, /// Configuration parameters for the solc compiler. #[clap(flatten, next_help_heading = "Solc Configuration")] pub solc_configuration: SolcConfiguration, /// Configuration parameters for the resolc compiler. #[clap(flatten, next_help_heading = "Resolc Configuration")] pub resolc_configuration: ResolcConfiguration, /// Configuration parameters for the Zombienet. #[clap(flatten, next_help_heading = "Zombienet Configuration")] pub zombienet_configuration: ZombieNetConfiguration, /// Configuration parameters for the geth node. #[clap(flatten, next_help_heading = "Geth Configuration")] pub geth_configuration: GethConfiguration, /// Configuration parameters for the lighthouse node. #[clap(flatten, next_help_heading = "Lighthouse Configuration")] pub lighthouse_configuration: KurtosisConfiguration, /// Configuration parameters for the Kitchensink. #[clap(flatten, next_help_heading = "Kitchensink Configuration")] pub kitchensink_configuration: KitchensinkConfiguration, /// Configuration parameters for the Revive Dev Node. #[clap(flatten, next_help_heading = "Revive Dev Node Configuration")] pub revive_dev_node_configuration: ReviveDevNodeConfiguration, /// Configuration parameters for the Eth Rpc. #[clap(flatten, next_help_heading = "Eth RPC Configuration")] pub eth_rpc_configuration: EthRpcConfiguration, /// Configuration parameters for the genesis. #[clap(flatten, next_help_heading = "Genesis Configuration")] pub genesis_configuration: GenesisConfiguration, /// Configuration parameters for the wallet. #[clap(flatten, next_help_heading = "Wallet Configuration")] pub wallet_configuration: WalletConfiguration, /// Configuration parameters for concurrency. #[clap(flatten, next_help_heading = "Concurrency Configuration")] pub concurrency_configuration: ConcurrencyConfiguration, /// Configuration parameters for the compilers and compilation. #[clap(flatten, next_help_heading = "Compilation Configuration")] pub compilation_configuration: CompilationConfiguration, /// Configuration parameters for the report. #[clap(flatten, next_help_heading = "Report Configuration")] pub report_configuration: ReportConfiguration, } impl Default for TestExecutionContext { fn default() -> Self { Self::parse_from(["execution-context"]) } } impl AsRef for TestExecutionContext { fn as_ref(&self) -> &WorkingDirectoryConfiguration { &self.working_directory } } impl AsRef for TestExecutionContext { fn as_ref(&self) -> &SolcConfiguration { &self.solc_configuration } } impl AsRef for TestExecutionContext { fn as_ref(&self) -> &ResolcConfiguration { &self.resolc_configuration } } impl AsRef for TestExecutionContext { fn as_ref(&self) -> &GethConfiguration { &self.geth_configuration } } impl AsRef for TestExecutionContext { fn as_ref(&self) -> &ZombieNetConfiguration { &self.zombienet_configuration } } impl AsRef for TestExecutionContext { fn as_ref(&self) -> &KurtosisConfiguration { &self.lighthouse_configuration } } impl AsRef for TestExecutionContext { fn as_ref(&self) -> &KitchensinkConfiguration { &self.kitchensink_configuration } } impl AsRef for TestExecutionContext { fn as_ref(&self) -> &ReviveDevNodeConfiguration { &self.revive_dev_node_configuration } } impl AsRef for TestExecutionContext { fn as_ref(&self) -> &EthRpcConfiguration { &self.eth_rpc_configuration } } impl AsRef for TestExecutionContext { fn as_ref(&self) -> &GenesisConfiguration { &self.genesis_configuration } } impl AsRef for TestExecutionContext { fn as_ref(&self) -> &WalletConfiguration { &self.wallet_configuration } } impl AsRef for TestExecutionContext { fn as_ref(&self) -> &ConcurrencyConfiguration { &self.concurrency_configuration } } impl AsRef for TestExecutionContext { fn as_ref(&self) -> &CompilationConfiguration { &self.compilation_configuration } } impl AsRef for TestExecutionContext { fn as_ref(&self) -> &ReportConfiguration { &self.report_configuration } } /// A set of configuration parameters for Solc. #[derive(Clone, Debug, Parser, Serialize)] pub struct SolcConfiguration { /// Specifies the default version of the Solc compiler that should be used if there is no /// override specified by one of the test cases. #[clap(long = "solc.version", default_value = "0.8.29")] pub version: Version, } /// A set of configuration parameters for Resolc. #[derive(Clone, Debug, Parser, Serialize)] pub struct ResolcConfiguration { /// Specifies the path of the resolc compiler to be used by the tool. /// /// If this is not specified, then the tool assumes that it should use the resolc binary that's /// provided in the user's $PATH. #[clap(id = "resolc.path", long = "resolc.path", default_value = "resolc")] pub path: PathBuf, } /// A set of configuration parameters for Zombienet. #[derive(Clone, Debug, Parser, Serialize)] pub struct ZombieNetConfiguration { /// Specifies the path of the zombienet node to be used by the tool. /// /// If this is not specified, then the tool assumes that it should use the zombienet binary /// that's provided in the user's $PATH. #[clap( id = "zombienet.path", long = "zombienet.path", default_value = "polkadot-parachain" )] pub path: PathBuf, /// The amount of time to wait upon startup before considering that the node timed out. #[clap( id = "zombienet.start-timeout-ms", long = "zombienet.start-timeout-ms", default_value = "5000", value_parser = parse_duration )] pub start_timeout_ms: Duration, /// This configures the tool to use Zombienet instead of using the revive-dev-node. #[clap(long = "zombienet.dont-use-dev-node")] pub use_zombienet: bool, } /// A set of configuration parameters for Geth. #[derive(Clone, Debug, Parser, Serialize)] pub struct GethConfiguration { /// Specifies the path of the geth node to be used by the tool. /// /// If this is not specified, then the tool assumes that it should use the geth binary that's /// provided in the user's $PATH. #[clap(id = "geth.path", long = "geth.path", default_value = "geth")] pub path: PathBuf, /// The amount of time to wait upon startup before considering that the node timed out. #[clap( id = "geth.start-timeout-ms", long = "geth.start-timeout-ms", default_value = "30000", value_parser = parse_duration )] pub start_timeout_ms: Duration, } /// A set of configuration parameters for kurtosis. #[derive(Clone, Debug, Parser, Serialize)] pub struct KurtosisConfiguration { /// Specifies the path of the kurtosis node to be used by the tool. /// /// If this is not specified, then the tool assumes that it should use the kurtosis binary that's /// provided in the user's $PATH. #[clap( id = "kurtosis.path", long = "kurtosis.path", default_value = "kurtosis" )] pub path: PathBuf, } /// A set of configuration parameters for Kitchensink. #[derive(Clone, Debug, Parser, Serialize)] pub struct KitchensinkConfiguration { /// Specifies the path of the kitchensink node to be used by the tool. /// /// If this is not specified, then the tool assumes that it should use the kitchensink binary /// that's provided in the user's $PATH. #[clap( id = "kitchensink.path", long = "kitchensink.path", default_value = "substrate-node" )] pub path: PathBuf, /// The amount of time to wait upon startup before considering that the node timed out. #[clap( id = "kitchensink.start-timeout-ms", long = "kitchensink.start-timeout-ms", default_value = "30000", value_parser = parse_duration )] pub start_timeout_ms: Duration, /// This configures the tool to use Kitchensink instead of using the revive-dev-node. #[clap(long = "kitchensink.dont-use-dev-node")] pub use_kitchensink: bool, } /// A set of configuration parameters for the revive dev node. #[derive(Clone, Debug, Parser, Serialize)] pub struct ReviveDevNodeConfiguration { /// Specifies the path of the revive dev node to be used by the tool. /// /// If this is not specified, then the tool assumes that it should use the revive dev node binary /// that's provided in the user's $PATH. #[clap( id = "revive-dev-node.path", long = "revive-dev-node.path", default_value = "revive-dev-node" )] pub path: PathBuf, /// The amount of time to wait upon startup before considering that the node timed out. #[clap( id = "revive-dev-node.start-timeout-ms", long = "revive-dev-node.start-timeout-ms", default_value = "30000", value_parser = parse_duration )] pub start_timeout_ms: Duration, } /// A set of configuration parameters for the ETH RPC. #[derive(Clone, Debug, Parser, Serialize)] pub struct EthRpcConfiguration { /// Specifies the path of the ETH RPC to be used by the tool. /// /// If this is not specified, then the tool assumes that it should use the ETH RPC binary /// that's provided in the user's $PATH. #[clap(id = "eth-rpc.path", long = "eth-rpc.path", default_value = "eth-rpc")] pub path: PathBuf, /// The amount of time to wait upon startup before considering that the node timed out. #[clap( id = "eth-rpc.start-timeout-ms", long = "eth-rpc.start-timeout-ms", default_value = "30000", value_parser = parse_duration )] pub start_timeout_ms: Duration, } /// A set of configuration parameters for the genesis. #[derive(Clone, Debug, Parser, Serialize)] pub struct GenesisConfiguration { /// Specifies the path of the genesis file to use for the nodes that are started. /// /// This is expected to be the path of a JSON geth genesis file. #[clap(id = "genesis.path", long = "genesis.path")] path: Option, /// The genesis object found at the provided path. #[clap(skip)] #[serde(skip)] genesis: OnceLock, } impl GenesisConfiguration { pub fn genesis(&self) -> anyhow::Result<&Genesis> { static DEFAULT_GENESIS: LazyLock = LazyLock::new(|| { let genesis = include_str!("../../../assets/dev-genesis.json"); serde_json::from_str(genesis).unwrap() }); match self.genesis.get() { Some(genesis) => Ok(genesis), None => { let genesis = match self.path.as_ref() { Some(genesis_path) => { let genesis_content = read_to_string(genesis_path)?; serde_json::from_str(genesis_content.as_str())? } None => DEFAULT_GENESIS.clone(), }; Ok(self.genesis.get_or_init(|| genesis)) } } } } /// A set of configuration parameters for the wallet. #[derive(Clone, Debug, Parser, Serialize)] pub struct WalletConfiguration { /// The private key of the default signer. #[clap( long = "wallet.default-private-key", default_value = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" )] #[serde(serialize_with = "serialize_private_key")] default_key: PrivateKeySigner, /// 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. #[clap(long = "wallet.additional-keys", default_value_t = 200)] additional_keys: usize, /// The wallet object that will be used. #[clap(skip)] #[serde(skip)] wallet: OnceLock>, } impl WalletConfiguration { pub fn wallet(&self) -> Arc { self.wallet .get_or_init(|| { let mut wallet = EthereumWallet::new(self.default_key.clone()); for signer in (1..=self.additional_keys) .map(|id| U256::from(id)) .map(|id| id.to_be_bytes::<32>()) .map(|id| PrivateKeySigner::from_bytes(&FixedBytes(id)).unwrap()) { wallet.register_signer(signer); } Arc::new(wallet) }) .clone() } pub fn highest_private_key_exclusive(&self) -> U256 { U256::try_from(self.additional_keys).unwrap() } } fn serialize_private_key(value: &PrivateKeySigner, serializer: S) -> Result where S: Serializer, { value.to_bytes().encode_hex().serialize(serializer) } /// A set of configuration for concurrency. #[derive(Clone, Debug, Parser, Serialize)] pub struct ConcurrencyConfiguration { /// Determines the amount of nodes that will be spawned for each chain. #[clap(long = "concurrency.number-of-nodes", default_value_t = 5)] pub number_of_nodes: usize, /// Determines the amount of tokio worker threads that will will be used. #[arg( long = "concurrency.number-of-threads", 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 = "concurrency.number-of-concurrent-tasks")] number_concurrent_tasks: Option, /// Determines if the concurrency limit should be ignored or not. #[arg(long = "concurrency.ignore-concurrency-limit")] ignore_concurrency_limit: bool, } impl ConcurrencyConfiguration { pub fn concurrency_limit(&self) -> Option { match self.ignore_concurrency_limit { true => None, false => Some( self.number_concurrent_tasks .unwrap_or(20 * self.number_of_nodes), ), } } } #[derive(Clone, Debug, Parser, Serialize)] pub struct CompilationConfiguration { /// Controls if the compilation cache should be invalidated or not. #[arg(long = "compilation.invalidate-cache")] pub invalidate_compilation_cache: bool, } #[derive(Clone, Debug, Parser, Serialize)] pub struct ReportConfiguration { /// Controls if the compiler input is included in the final report. #[clap(long = "report.include-compiler-input")] pub include_compiler_input: bool, /// Controls if the compiler output is included in the final report. #[clap(long = "report.include-compiler-output")] pub include_compiler_output: bool, } /// Represents the working directory that the program uses. #[derive(Debug, Clone)] pub enum WorkingDirectoryConfiguration { /// A temporary directory is used as the working directory. This will be removed when dropped. TemporaryDirectory(Arc), /// A directory with a path is used as the working directory. Path(PathBuf), } impl WorkingDirectoryConfiguration { pub fn as_path(&self) -> &Path { self.as_ref() } } impl Deref for WorkingDirectoryConfiguration { type Target = Path; fn deref(&self) -> &Self::Target { self.as_path() } } impl AsRef for WorkingDirectoryConfiguration { fn as_ref(&self) -> &Path { match self { WorkingDirectoryConfiguration::TemporaryDirectory(temp_dir) => temp_dir.path(), WorkingDirectoryConfiguration::Path(path) => path.as_path(), } } } impl Default for WorkingDirectoryConfiguration { fn default() -> Self { TempDir::new() .map(Arc::new) .map(Self::TemporaryDirectory) .expect("Failed to create the temporary directory") } } impl FromStr for WorkingDirectoryConfiguration { type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s { "" => Ok(Default::default()), _ => Ok(Self::Path(PathBuf::from(s))), } } } impl Display for WorkingDirectoryConfiguration { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Display::fmt(&self.as_path().display(), f) } } impl Serialize for WorkingDirectoryConfiguration { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { self.as_path().serialize(serializer) } } fn parse_duration(s: &str) -> anyhow::Result { u64::from_str(s) .map(Duration::from_millis) .map_err(Into::into) } /// The Solidity compatible node implementation. /// /// This describes the solutions to be tested against on a high level. #[derive( Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, ValueEnum, EnumString, Display, AsRefStr, IntoStaticStr, )] #[strum(serialize_all = "kebab-case")] pub enum TestingPlatform { /// The go-ethereum reference full node EVM implementation. Geth, /// The kitchensink runtime provides the PolkaVM (PVM) based node implementation. Kitchensink, /// A polkadot/Substrate based network ZombieNet, }