From 0b97d7dc2949984380576ae80ad4106a81dd4691 Mon Sep 17 00:00:00 2001 From: Omar Date: Fri, 18 Jul 2025 15:06:40 +0300 Subject: [PATCH 1/2] Support other matterlabs variables (#43) * Introduce a custom kitchensink network * fix formatting * Added `--dev` to `substrate-node` arguments. This commit adds the `--dev` argument to the `substrate-node` to allow the chain to keep advancing as time goes own. We have found that if this option is not added then the chain won't advance forward. * fix clippy warning * fix clippy warning * Fix function selector and argument encoding * Avoid extra buffer allocation * Remove reliance on the web3 crate * Update the async runtime with syntactic sugar. * Fix tests * Fix doc test * Give nodes a standard way to get their alloy provider * Add ability to get the chain_id from node * Get kitchensink provider to use kitchensink network * Use provider method in tests * Add support for getting the gas limit from the node * Add a way to get the coinbase address * Add a way to get the block difficulty from the node * Add a way to get block info from the node * Expose APIs for getting the info of a specific block * Add resolution logic for other matterlabs variables * Fix tests * Add comment on alternative solutions * Change kitchensink gas limit assertion * Remove un-needed profile config --- Cargo.lock | 1 + crates/core/src/driver/mod.rs | 38 ++- crates/format/Cargo.toml | 2 + crates/format/src/input.rs | 260 ++++++++++++++-- .../node-interaction/src/blocking_executor.rs | 1 - crates/node-interaction/src/lib.rs | 38 ++- crates/node/src/geth.rs | 235 +++++++++++++-- crates/node/src/kitchensink.rs | 282 +++++++++++++++--- 8 files changed, 748 insertions(+), 109 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b7cfb19..a8c4f41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3984,6 +3984,7 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "anyhow", + "revive-dt-node-interaction", "semver 1.0.26", "serde", "serde_json", diff --git a/crates/core/src/driver/mod.rs b/crates/core/src/driver/mod.rs index f4d6acc..bed8642 100644 --- a/crates/core/src/driver/mod.rs +++ b/crates/core/src/driver/mod.rs @@ -1,7 +1,7 @@ //! The test driver handles the compilation and execution of the test cases. use alloy::json_abi::JsonAbi; -use alloy::network::TransactionBuilder; +use alloy::network::{Ethereum, TransactionBuilder}; use alloy::rpc::types::TransactionReceipt; use alloy::rpc::types::trace::geth::GethTrace; use alloy::{ @@ -134,17 +134,21 @@ where std::any::type_name::() ); - let tx = - match input.legacy_transaction(nonce, &self.deployed_contracts, &self.deployed_abis) { - Ok(tx) => { - tracing::debug!("Legacy transaction data: {tx:#?}"); - tx - } - Err(err) => { - tracing::error!("Failed to construct legacy transaction: {err:?}"); - return Err(err); - } - }; + let tx = match input.legacy_transaction( + nonce, + &self.deployed_contracts, + &self.deployed_abis, + node, + ) { + Ok(tx) => { + tracing::debug!("Legacy transaction data: {tx:#?}"); + tx + } + Err(err) => { + tracing::error!("Failed to construct legacy transaction: {err:?}"); + return Err(err); + } + }; tracing::trace!("Executing transaction for input: {input:?}"); @@ -253,10 +257,12 @@ where return Err(error.into()); } }; - let tx = TransactionRequest::default() - .nonce(nonce) - .from(input.caller) - .with_deploy_code(code); + let tx = { + let tx = TransactionRequest::default() + .nonce(nonce) + .from(input.caller); + TransactionBuilder::::with_deploy_code(tx, code) + }; let receipt = match node.execute_transaction(tx) { Ok(receipt) => receipt, diff --git a/crates/format/Cargo.toml b/crates/format/Cargo.toml index 4352683..c1b7674 100644 --- a/crates/format/Cargo.toml +++ b/crates/format/Cargo.toml @@ -9,6 +9,8 @@ repository.workspace = true rust-version.workspace = true [dependencies] +revive-dt-node-interaction = { workspace = true } + alloy = { workspace = true } alloy-primitives = { workspace = true } alloy-sol-types = { workspace = true } diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index 9275dca..1cdefe4 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use alloy::{ + eips::BlockNumberOrTag, json_abi::JsonAbi, network::TransactionBuilder, primitives::{Address, Bytes, U256}, @@ -10,6 +11,8 @@ use semver::VersionReq; use serde::Deserialize; use serde_json::Value; +use revive_dt_node_interaction::EthereumNode; + #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] pub struct Input { #[serde(default = "default_caller")] @@ -84,6 +87,7 @@ impl Input { &self, deployed_abis: &HashMap, deployed_contracts: &HashMap, + chain_state_provider: &impl EthereumNode, ) -> anyhow::Result { let Method::FunctionName(ref function_name) = self.method else { return Ok(Bytes::default()); // fallback or deployer — no input @@ -145,7 +149,7 @@ impl Input { calldata.extend(function.selector().0); for (arg_idx, arg) in calldata_args.iter().enumerate() { - match resolve_argument(arg, deployed_contracts) { + match resolve_argument(arg, deployed_contracts, chain_state_provider) { Ok(resolved) => { calldata.extend(resolved.to_be_bytes::<32>()); } @@ -165,8 +169,10 @@ impl Input { nonce: u64, deployed_contracts: &HashMap, deployed_abis: &HashMap, + chain_state_provider: &impl EthereumNode, ) -> anyhow::Result { - let input_data = self.encoded_input(deployed_abis, deployed_contracts)?; + let input_data = + self.encoded_input(deployed_abis, deployed_contracts, chain_state_provider)?; let transaction_request = TransactionRequest::default().nonce(nonce); match self.method { Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)), @@ -196,6 +202,7 @@ fn default_caller() -> Address { fn resolve_argument( value: &str, deployed_contracts: &HashMap, + chain_state_provider: &impl EthereumNode, ) -> anyhow::Result { if let Some(instance) = value.strip_suffix(".address") { Ok(U256::from_be_slice( @@ -217,30 +224,40 @@ fn resolve_argument( } else if let Some(value) = value.strip_prefix("0x") { Ok(U256::from_str_radix(value, 16) .map_err(|error| anyhow::anyhow!("Invalid hexadecimal literal: {}", error))?) - } else { - // TODO: This is a set of "variables" that we need to be able to resolve to be fully in - // compliance with the matter labs tester but we currently do not resolve them. We need to - // add logic that does their resolution in the future, perhaps through some kind of system - // context API that we pass down to the resolution function that allows it to make calls to - // the node to perform these resolutions. - let is_unsupported = [ - "$CHAIN_ID", - "$GAS_LIMIT", - "$COINBASE", - "$DIFFICULTY", - "$BLOCK_HASH", - "$BLOCK_TIMESTAMP", - ] - .iter() - .any(|var| value.starts_with(var)); + } else if value == "$CHAIN_ID" { + let chain_id = chain_state_provider.chain_id()?; + Ok(U256::from(chain_id)) + } else if value == "$GAS_LIMIT" { + let gas_limit = chain_state_provider.block_gas_limit(BlockNumberOrTag::Latest)?; + Ok(U256::from(gas_limit)) + } else if value == "$COINBASE" { + let coinbase = chain_state_provider.block_coinbase(BlockNumberOrTag::Latest)?; + Ok(U256::from_be_slice(coinbase.as_ref())) + } else if value == "$DIFFICULTY" { + let block_difficulty = chain_state_provider.block_difficulty(BlockNumberOrTag::Latest)?; + Ok(block_difficulty) + } else if value.starts_with("$BLOCK_HASH") { + let offset: u64 = value + .split(':') + .next_back() + .and_then(|value| value.parse().ok()) + .unwrap_or_default(); - if is_unsupported { - tracing::error!(value, "Unsupported variable used"); - anyhow::bail!("Encountered {value} which is currently unsupported by the framework"); - } else { - Ok(U256::from_str_radix(value, 10) - .map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?) - } + let current_block_number = chain_state_provider.last_block_number()?; + let desired_block_number = current_block_number - offset; + + let block_hash = chain_state_provider.block_hash(desired_block_number.into())?; + + Ok(U256::from_be_bytes(block_hash.0)) + } else if value == "$BLOCK_NUMBER" { + let current_block_number = chain_state_provider.last_block_number()?; + Ok(U256::from(current_block_number)) + } else if value == "$BLOCK_TIMESTAMP" { + let timestamp = chain_state_provider.block_timestamp(BlockNumberOrTag::Latest)?; + Ok(U256::from(timestamp)) + } else { + Ok(U256::from_str_radix(value, 10) + .map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?) } } @@ -253,6 +270,69 @@ mod tests { use alloy_sol_types::SolValue; use std::collections::HashMap; + struct DummyEthereumNode; + + impl EthereumNode for DummyEthereumNode { + fn execute_transaction( + &self, + _: TransactionRequest, + ) -> anyhow::Result { + unimplemented!() + } + + fn trace_transaction( + &self, + _: alloy::rpc::types::TransactionReceipt, + ) -> anyhow::Result { + unimplemented!() + } + + fn state_diff( + &self, + _: alloy::rpc::types::TransactionReceipt, + ) -> anyhow::Result { + unimplemented!() + } + + fn fetch_add_nonce(&self, _: Address) -> anyhow::Result { + unimplemented!() + } + + fn chain_id(&self) -> anyhow::Result { + Ok(0x123) + } + + fn block_gas_limit(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result { + Ok(0x1234) + } + + fn block_coinbase(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result
{ + Ok(Address::ZERO) + } + + fn block_difficulty(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result { + Ok(U256::from(0x12345u128)) + } + + fn block_hash( + &self, + _: alloy::eips::BlockNumberOrTag, + ) -> anyhow::Result { + Ok([0xEE; 32].into()) + } + + fn block_timestamp( + &self, + _: alloy::eips::BlockNumberOrTag, + ) -> anyhow::Result { + Ok(0x123456) + } + + fn last_block_number(&self) -> anyhow::Result { + Ok(0x1234567) + } + } + #[test] fn test_encoded_input_uint256() { let raw_metadata = r#" @@ -288,7 +368,7 @@ mod tests { let deployed_contracts = HashMap::new(); let encoded = input - .encoded_input(&deployed_abis, &deployed_contracts) + .encoded_input(&deployed_abis, &deployed_contracts, &DummyEthereumNode) .unwrap(); assert!(encoded.0.starts_with(&selector)); @@ -331,7 +411,9 @@ mod tests { abis.insert("Contract".to_string(), parsed_abi); let contracts = HashMap::new(); - let encoded = input.encoded_input(&abis, &contracts).unwrap(); + let encoded = input + .encoded_input(&abis, &contracts, &DummyEthereumNode) + .unwrap(); assert!(encoded.0.starts_with(&selector)); type T = (alloy_primitives::Address,); @@ -341,4 +423,128 @@ mod tests { address!("0x1000000000000000000000000000000000000001") ); } + + #[test] + fn resolver_can_resolve_chain_id_variable() { + // Arrange + let input = "$CHAIN_ID"; + + // Act + let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!(resolved, U256::from(DummyEthereumNode.chain_id().unwrap())) + } + + #[test] + fn resolver_can_resolve_gas_limit_variable() { + // Arrange + let input = "$GAS_LIMIT"; + + // Act + let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!( + resolved, + U256::from( + DummyEthereumNode + .block_gas_limit(Default::default()) + .unwrap() + ) + ) + } + + #[test] + fn resolver_can_resolve_coinbase_variable() { + // Arrange + let input = "$COINBASE"; + + // Act + let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!( + resolved, + U256::from_be_slice( + DummyEthereumNode + .block_coinbase(Default::default()) + .unwrap() + .as_ref() + ) + ) + } + + #[test] + fn resolver_can_resolve_block_difficulty_variable() { + // Arrange + let input = "$DIFFICULTY"; + + // Act + let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!( + resolved, + DummyEthereumNode + .block_difficulty(Default::default()) + .unwrap() + ) + } + + #[test] + fn resolver_can_resolve_block_hash_variable() { + // Arrange + let input = "$BLOCK_HASH"; + + // Act + let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!( + resolved, + U256::from_be_bytes(DummyEthereumNode.block_hash(Default::default()).unwrap().0) + ) + } + + #[test] + fn resolver_can_resolve_block_number_variable() { + // Arrange + let input = "$BLOCK_NUMBER"; + + // Act + let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!( + resolved, + U256::from(DummyEthereumNode.last_block_number().unwrap()) + ) + } + + #[test] + fn resolver_can_resolve_block_timestamp_variable() { + // Arrange + let input = "$BLOCK_TIMESTAMP"; + + // Act + let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!( + resolved, + U256::from( + DummyEthereumNode + .block_timestamp(Default::default()) + .unwrap() + ) + ) + } } diff --git a/crates/node-interaction/src/blocking_executor.rs b/crates/node-interaction/src/blocking_executor.rs index 043dd19..baba0c1 100644 --- a/crates/node-interaction/src/blocking_executor.rs +++ b/crates/node-interaction/src/blocking_executor.rs @@ -144,7 +144,6 @@ impl BlockingExecutor { } } } - /// 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. diff --git a/crates/node-interaction/src/lib.rs b/crates/node-interaction/src/lib.rs index 2c2eef5..afba76a 100644 --- a/crates/node-interaction/src/lib.rs +++ b/crates/node-interaction/src/lib.rs @@ -1,8 +1,10 @@ //! This crate implements all node interactions. -use alloy::primitives::Address; +use alloy::eips::BlockNumberOrTag; +use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256}; use alloy::rpc::types::trace::geth::{DiffMode, GethTrace}; use alloy::rpc::types::{TransactionReceipt, TransactionRequest}; +use anyhow::Result; mod blocking_executor; pub use blocking_executor::*; @@ -10,17 +12,37 @@ pub use blocking_executor::*; /// 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, - ) -> anyhow::Result; + fn execute_transaction(&self, transaction: TransactionRequest) -> Result; /// Trace the transaction in the [TransactionReceipt] and return a [GethTrace]. - fn trace_transaction(&self, transaction: TransactionReceipt) -> anyhow::Result; + fn trace_transaction(&self, transaction: TransactionReceipt) -> Result; /// Returns the state diff of the transaction hash in the [TransactionReceipt]. - fn state_diff(&self, transaction: TransactionReceipt) -> anyhow::Result; + fn state_diff(&self, transaction: TransactionReceipt) -> Result; /// Returns the next available nonce for the given [Address]. - fn fetch_add_nonce(&self, address: Address) -> anyhow::Result; + fn fetch_add_nonce(&self, address: Address) -> Result; + + /// Returns the ID of the chain that the node is on. + fn chain_id(&self) -> Result; + + // 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; + + /// Returns the coinbase of the specified block. + fn block_coinbase(&self, number: BlockNumberOrTag) -> Result
; + + /// Returns the difficulty of the specified block. + fn block_difficulty(&self, number: BlockNumberOrTag) -> Result; + + /// Returns the hash of the specified block. + fn block_hash(&self, number: BlockNumberOrTag) -> Result; + + /// Returns the timestamp of the specified block, + fn block_timestamp(&self, number: BlockNumberOrTag) -> Result; + + /// Returns the number of the last block. + fn last_block_number(&self) -> Result; } diff --git a/crates/node/src/geth.rs b/crates/node/src/geth.rs index 57cb2db..4efd626 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -14,9 +14,14 @@ use std::{ }; use alloy::{ - network::EthereumWallet, - primitives::Address, - providers::{Provider, ProviderBuilder, ext::DebugApi}, + eips::BlockNumberOrTag, + network::{Ethereum, EthereumWallet}, + primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, U256}, + providers::{ + Provider, ProviderBuilder, + ext::DebugApi, + fillers::{FillProvider, TxFiller}, + }, rpc::types::{ TransactionReceipt, TransactionRequest, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, @@ -191,6 +196,24 @@ impl Instance { fn geth_stderr_log_file_path(&self) -> PathBuf { self.logs_directory.join(Self::GETH_STDERR_LOG_FILE_NAME) } + + fn provider( + &self, + ) -> impl Future< + Output = anyhow::Result< + FillProvider, impl Provider, Ethereum>, + >, + > + 'static { + let connection_string = self.connection_string(); + let wallet = self.wallet.clone(); + Box::pin(async move { + ProviderBuilder::new() + .wallet(wallet) + .connect(&connection_string) + .await + .map_err(Into::into) + }) + } } impl EthereumNode for Instance { @@ -199,17 +222,12 @@ impl EthereumNode for Instance { &self, transaction: TransactionRequest, ) -> anyhow::Result { - let connection_string = self.connection_string(); - let wallet = self.wallet.clone(); - + 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 = ProviderBuilder::new() - .wallet(wallet) - .connect(&connection_string) - .await?; + let provider = provider.await?; let pending_transaction = provider.send_transaction(transaction).await?; let transaction_hash = pending_transaction.tx_hash(); @@ -289,18 +307,15 @@ impl EthereumNode for Instance { &self, transaction: TransactionReceipt, ) -> anyhow::Result { - let connection_string = self.connection_string(); let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig { diff_mode: Some(true), disable_code: None, disable_storage: None, }); - let wallet = self.wallet.clone(); + let provider = self.provider(); BlockingExecutor::execute(async move { - Ok(ProviderBuilder::new() - .wallet(wallet) - .connect(&connection_string) + Ok(provider .await? .debug_trace_transaction(transaction.transaction_hash, trace_options) .await?) @@ -323,13 +338,9 @@ impl EthereumNode for Instance { #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] fn fetch_add_nonce(&self, address: Address) -> anyhow::Result { - let connection_string = self.connection_string.clone(); - let wallet = self.wallet.clone(); - + let provider = self.provider(); let onchain_nonce = BlockingExecutor::execute::>(async move { - ProviderBuilder::new() - .wallet(wallet) - .connect(&connection_string) + provider .await? .get_transaction_count(address) .await @@ -342,6 +353,87 @@ impl EthereumNode for Instance { *current += 1; Ok(value) } + + #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] + fn chain_id(&self) -> anyhow::Result { + let provider = self.provider(); + BlockingExecutor::execute(async move { + 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 { + let provider = self.provider(); + BlockingExecutor::execute(async move { + 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
{ + let provider = self.provider(); + BlockingExecutor::execute(async move { + 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 { + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider + .await? + .get_block_by_number(number) + .await? + .ok_or(anyhow::Error::msg("Blockchain has no blocks")) + .map(|block| block.header.difficulty) + })? + } + + #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] + fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result { + let provider = self.provider(); + BlockingExecutor::execute(async move { + 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 { + let provider = self.provider(); + BlockingExecutor::execute(async move { + 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 { + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider.await?.get_block_number().await.map_err(Into::into) + })? + } } impl Node for Instance { @@ -429,7 +521,7 @@ mod tests { use crate::{GENESIS_JSON, Node}; - use super::Instance; + use super::*; fn test_config() -> (Arguments, TempDir) { let mut config = Arguments::default(); @@ -439,6 +531,16 @@ mod tests { (config, temp_dir) } + fn new_node() -> (Instance, TempDir) { + let (args, temp_dir) = test_config(); + let mut node = Instance::new(&args); + node.init(GENESIS_JSON.to_owned()) + .expect("Failed to initialize the node") + .spawn_process() + .expect("Failed to spawn the node process"); + (node, temp_dir) + } + #[test] fn init_works() { Instance::new(&test_config().0) @@ -461,4 +563,93 @@ mod tests { "expected version string, got: '{version}'" ); } + + #[test] + fn can_get_chain_id_from_node() { + // Arrange + let (node, _temp_dir) = new_node(); + + // Act + let chain_id = node.chain_id(); + + // 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() { + // Arrange + let (node, _temp_dir) = new_node(); + + // Act + let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest); + + // 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() { + // Arrange + let (node, _temp_dir) = new_node(); + + // Act + let coinbase = node.block_coinbase(BlockNumberOrTag::Latest); + + // 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() { + // Arrange + let (node, _temp_dir) = new_node(); + + // Act + let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest); + + // 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() { + // Arrange + let (node, _temp_dir) = new_node(); + + // Act + let block_hash = node.block_hash(BlockNumberOrTag::Latest); + + // Assert + let _ = block_hash.expect("Failed to get the block hash"); + } + + #[test] + fn can_get_block_timestamp_from_node() { + // Arrange + let (node, _temp_dir) = new_node(); + + // Act + let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest); + + // Assert + let _ = block_timestamp.expect("Failed to get the block timestamp"); + } + + #[test] + fn can_get_block_number_from_node() { + // Arrange + let (node, _temp_dir) = new_node(); + + // Act + let block_number = node.last_block_number(); + + // Assert + let block_number = block_number.expect("Failed to get the block number"); + assert_eq!(block_number, 0) + } } diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index fbb36d4..995b8ca 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -13,13 +13,18 @@ use std::{ use alloy::{ consensus::{BlockHeader, TxEnvelope}, + eips::BlockNumberOrTag, hex, network::{ Ethereum, EthereumWallet, Network, TransactionBuilder, TransactionBuilderError, UnbuiltTransactionError, }, - primitives::{Address, B64, B256, BlockNumber, Bloom, Bytes, U256}, - providers::{Provider, ProviderBuilder, ext::DebugApi}, + primitives::{Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, U256}, + providers::{ + Provider, ProviderBuilder, + ext::DebugApi, + fillers::{FillProvider, TxFiller}, + }, rpc::types::{ TransactionReceipt, eth::{Block, Header, Transaction}, @@ -232,6 +237,7 @@ impl KitchensinkNode { Ok(()) } + #[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))] fn extract_balance_from_genesis_file( &self, @@ -330,6 +336,29 @@ impl KitchensinkNode { fn proxy_stderr_log_file_path(&self) -> PathBuf { self.logs_directory.join(Self::PROXY_STDERR_LOG_FILE_NAME) } + + fn provider( + &self, + ) -> impl Future< + Output = anyhow::Result< + FillProvider< + impl TxFiller, + impl Provider, + KitchenSinkNetwork, + >, + >, + > + 'static { + let connection_string = self.connection_string(); + let wallet = self.wallet.clone(); + Box::pin(async move { + ProviderBuilder::new() + .network::() + .wallet(wallet) + .connect(&connection_string) + .await + .map_err(Into::into) + }) + } } impl EthereumNode for KitchensinkNode { @@ -338,17 +367,10 @@ impl EthereumNode for KitchensinkNode { &self, transaction: alloy::rpc::types::TransactionRequest, ) -> anyhow::Result { - let url = self.rpc_url.clone(); - let wallet = self.wallet.clone(); - - tracing::debug!("Submitting transaction: {transaction:#?}"); - - tracing::info!("Submitting tx to kitchensink"); + tracing::debug!(?transaction, "Submitting transaction"); + let provider = self.provider(); let receipt = BlockingExecutor::execute(async move { - Ok(ProviderBuilder::new() - .network::() - .wallet(wallet) - .connect(&url) + Ok(provider .await? .send_transaction(transaction) .await? @@ -364,20 +386,15 @@ impl EthereumNode for KitchensinkNode { &self, transaction: TransactionReceipt, ) -> anyhow::Result { - let url = self.rpc_url.clone(); let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig { diff_mode: Some(true), disable_code: None, disable_storage: None, }); - - let wallet = self.wallet.clone(); + let provider = self.provider(); BlockingExecutor::execute(async move { - Ok(ProviderBuilder::new() - .network::() - .wallet(wallet) - .connect(&url) + Ok(provider .await? .debug_trace_transaction(transaction.transaction_hash, trace_options) .await?) @@ -397,13 +414,9 @@ impl EthereumNode for KitchensinkNode { #[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))] fn fetch_add_nonce(&self, address: Address) -> anyhow::Result { - let url = self.rpc_url.clone(); - let wallet = self.wallet.clone(); - + let provider = self.provider(); let onchain_nonce = BlockingExecutor::execute::>(async move { - ProviderBuilder::new() - .wallet(wallet) - .connect(&url) + provider .await? .get_transaction_count(address) .await @@ -416,6 +429,87 @@ impl EthereumNode for KitchensinkNode { *current += 1; Ok(value) } + + #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] + fn chain_id(&self) -> anyhow::Result { + let provider = self.provider(); + BlockingExecutor::execute(async move { + 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 { + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider + .await? + .get_block_by_number(number) + .await? + .ok_or(anyhow::Error::msg("Blockchain has no blocks")) + .map(|block| block.header.gas_limit) + })? + } + + #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] + fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result
{ + let provider = self.provider(); + BlockingExecutor::execute(async move { + 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 { + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider + .await? + .get_block_by_number(number) + .await? + .ok_or(anyhow::Error::msg("Blockchain has no blocks")) + .map(|block| block.header.difficulty) + })? + } + + #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] + fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result { + let provider = self.provider(); + BlockingExecutor::execute(async move { + 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 { + let provider = self.provider(); + BlockingExecutor::execute(async move { + 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 { + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider.await?.get_block_number().await.map_err(Into::into) + })? + } } impl Node for KitchensinkNode { @@ -926,6 +1020,7 @@ mod tests { use alloy::rpc::types::TransactionRequest; use revive_dt_config::Arguments; use std::path::PathBuf; + use std::sync::LazyLock; use temp_dir::TempDir; use std::fs; @@ -945,20 +1040,49 @@ mod tests { (config, temp_dir) } + fn new_node() -> (KitchensinkNode, Arguments, TempDir) { + // 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 + // nodes to be started in series and not in parallel. To do this, we use + // a dummy mutex here such that there can only be a single node being + // started up at any point of time. This will make our tests run slower + // but it will allow the node startup to not timeout. + // + // Note: an alternative to starting all of the nodes in series and not + // in parallel would be for us to reuse the same node between tests + // which is not the best thing to do in my opinion as it removes all + // of the isolation between tests and makes them depend on what other + // tests do. For example, if one test checks what the block number is + // and another test submits a transaction then the tx test would have + // side effects that affect the block number test. + static NODE_START_MUTEX: Mutex<()> = Mutex::new(()); + let _guard = NODE_START_MUTEX.lock().unwrap(); + + let (args, temp_dir) = 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) + } + + /// 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) + }); + &NODE.0 + } + #[tokio::test] async fn node_mines_simple_transfer_transaction_and_returns_receipt() { // Arrange - let (args, _temp_dir) = test_config(); - let mut node = KitchensinkNode::new(&args); - node.spawn(GENESIS_JSON.to_owned()) - .expect("Failed to spawn the node"); + let (node, args, _temp_dir) = new_node(); - let provider = ProviderBuilder::new() - .network::() - .wallet(args.wallet()) - .connect(&node.rpc_url) - .await - .expect("Failed to create provider"); + let provider = node.provider().await.expect("Failed to create provider"); let account_address = args.wallet().default_signer().address(); let transaction = TransactionRequest::default() @@ -1137,4 +1261,92 @@ mod tests { "Expected eth-rpc version string, got: {version}" ); } + + #[test] + fn can_get_chain_id_from_node() { + // Arrange + let node = shared_node(); + + // Act + let chain_id = node.chain_id(); + + // 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() { + // Arrange + let node = shared_node(); + + // Act + let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest); + + // Assert + let _ = gas_limit.expect("Failed to get the gas limit"); + } + + #[test] + fn can_get_coinbase_from_node() { + // Arrange + let node = shared_node(); + + // Act + let coinbase = node.block_coinbase(BlockNumberOrTag::Latest); + + // Assert + let coinbase = coinbase.expect("Failed to get the coinbase"); + assert_eq!(coinbase, Address::ZERO) + } + + #[test] + fn can_get_block_difficulty_from_node() { + // Arrange + let node = shared_node(); + + // Act + let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest); + + // 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() { + // Arrange + let node = shared_node(); + + // Act + let block_hash = node.block_hash(BlockNumberOrTag::Latest); + + // Assert + let _ = block_hash.expect("Failed to get the block hash"); + } + + #[test] + fn can_get_block_timestamp_from_node() { + // Arrange + let node = shared_node(); + + // Act + let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest); + + // Assert + let _ = block_timestamp.expect("Failed to get the block timestamp"); + } + + #[test] + fn can_get_block_number_from_node() { + // Arrange + let node = shared_node(); + + // Act + let block_number = node.last_block_number(); + + // Assert + let block_number = block_number.expect("Failed to get the block number"); + assert_eq!(block_number, 0) + } } From 22599423638c98eb59070fda9333b3f388032bc5 Mon Sep 17 00:00:00 2001 From: Omar Date: Fri, 18 Jul 2025 15:08:13 +0300 Subject: [PATCH 2/2] Cleanup execution logic (#45) * Introduce a custom kitchensink network * fix formatting * Added `--dev` to `substrate-node` arguments. This commit adds the `--dev` argument to the `substrate-node` to allow the chain to keep advancing as time goes own. We have found that if this option is not added then the chain won't advance forward. * fix clippy warning * fix clippy warning * Fix the ABI finding logic * Fix function selector and argument encoding * Avoid extra buffer allocation * Remove reliance on the web3 crate * Implement ABI fix in the compiler trait impl * Update the async runtime with syntactic sugar. * Fix tests * Fix doc test * Give nodes a standard way to get their alloy provider * Add ability to get the chain_id from node * Get kitchensink provider to use kitchensink network * Use provider method in tests * Add support for getting the gas limit from the node * Add a way to get the coinbase address * Add a way to get the block difficulty from the node * Add a way to get block info from the node * Expose APIs for getting the info of a specific block * Add resolution logic for other matterlabs variables * Fix tests * Add comment on alternative solutions * Change kitchensink gas limit assertion * Cleanup execution logic --- crates/core/src/driver/mod.rs | 439 ++++++++++++++++++++++++++++++---- crates/core/src/main.rs | 33 +-- 2 files changed, 408 insertions(+), 64 deletions(-) diff --git a/crates/core/src/driver/mod.rs b/crates/core/src/driver/mod.rs index bed8642..cca0be0 100644 --- a/crates/core/src/driver/mod.rs +++ b/crates/core/src/driver/mod.rs @@ -19,7 +19,7 @@ use revive_dt_report::reporter::{CompilationTask, Report, Span}; use revive_solc_json_interface::SolcStandardJsonOutput; use serde_json::Value; use std::collections::HashMap as StdHashMap; -use tracing::Level; +use std::fmt::Debug; use crate::Platform; @@ -404,66 +404,177 @@ where } } - pub fn execute(&mut self, span: Span) -> anyhow::Result<()> { + // A note on this function and the choice of how we handle errors that happen here. This is not + // a doc comment since it's a comment for the maintainers of this code and not for the users of + // this code. + // + // This function does a few things: it builds the contracts for the various SOLC modes needed. + // It deploys the contracts to the chain, and it executes the various inputs that are specified + // for the test cases. + // + // In most functions in the codebase, it's fine to just say "If we encounter an error just + // bubble it up to the caller", but this isn't a good idea to do here and we need an elaborate + // way to report errors all while being graceful and continuing execution where we can. For + // example, if one of the inputs of one of the cases fail to execute, then we should not just + // bubble that error up immediately. Instead, we should note it down and continue to the next + // case as the next case might succeed. + // + // Therefore, this method returns an `ExecutionResult` object, and not just a normal `Result`. + // This object is fully typed to contain information about what exactly in the execution was a + // success and what failed. + // + // The above then allows us to have better logging and better information in the caller of this + // function as we have a more detailed view of what worked and what didn't. + pub fn execute(&mut self, span: Span) -> ExecutionResult { + // This is the execution result object that all of the execution information will be + // collected into and returned at the end of the execution. + let mut execution_result = ExecutionResult::default(); + + let tracing_span = tracing::info_span!("Handling metadata file"); + let _guard = tracing_span.enter(); + for mode in self.metadata.solc_modes() { + let tracing_span = tracing::info_span!("With solc mode", solc_mode = ?mode); + let _guard = tracing_span.enter(); + let mut leader_state = State::::new(self.config, span); - leader_state.build_contracts(&mode, self.metadata)?; - let mut follower_state = State::::new(self.config, span); - follower_state.build_contracts(&mode, self.metadata)?; - for (case_idx, case) in self.metadata.cases.iter().enumerate() { - // Creating a tracing span to know which case within the metadata is being executed - // and which one we're getting logs for. - let tracing_span = tracing::span!( - Level::INFO, - "Executing case", - case = case.name, + // We build the contracts. If building the contracts for the metadata file fails then we + // have no other option but to keep note of this error and move on to the next solc mode + // and NOT just bail out of the execution as a whole. + let build_result = tracing::info_span!("Building contracts").in_scope(|| { + match leader_state.build_contracts(&mode, self.metadata) { + Ok(_) => { + tracing::debug!(target = ?Target::Leader, "Contract building succeeded"); + execution_result.add_successful_build(Target::Leader, mode.clone()); + }, + Err(error) => { + tracing::error!(target = ?Target::Leader, ?error, "Contract building failed"); + execution_result.add_failed_build(Target::Leader, mode.clone(), error); + return Err(()); + } + } + match follower_state.build_contracts(&mode, self.metadata) { + Ok(_) => { + tracing::debug!(target = ?Target::Follower, "Contract building succeeded"); + execution_result.add_successful_build(Target::Follower, mode.clone()); + }, + Err(error) => { + tracing::error!(target = ?Target::Follower, ?error, "Contract building failed"); + execution_result.add_failed_build(Target::Follower, mode.clone(), error); + return Err(()); + } + } + Ok(()) + }); + if build_result.is_err() { + // Note: We skip to the next solc mode as there's nothing that we can do at this + // point, the building has failed. We do NOT bail out of the execution as a whole. + continue; + } + + // For cases if one of the inputs fail then we move on to the next case and we do NOT + // bail out of the whole thing. + 'case_loop: for (case_idx, case) in self.metadata.cases.iter().enumerate() { + let tracing_span = tracing::info_span!( + "Handling case", + case_name = case.name, case_idx = case_idx ); let _guard = tracing_span.enter(); - for input in &case.inputs { - tracing::debug!("Starting deploying contract {}", &input.instance); - if let Err(err) = leader_state.deploy_contracts(input, self.leader_node) { - tracing::error!("Leader deployment failed for {}: {err}", input.instance); - continue; - } else { - tracing::debug!("Leader deployment succeeded for {}", &input.instance); + // For inputs if one of the inputs fail we move on to the next case (we do not move + // on to the next input as it doesn't make sense. It depends on the previous one). + for (input_idx, input) in case.inputs.iter().enumerate() { + let tracing_span = tracing::info_span!("Handling input", input_idx); + let _guard = tracing_span.enter(); + + // TODO: verify if this is correct, I doubt that we need to do contract redeploy + // for each input. It doesn't quite look to be correct but we need to cross + // check with the matterlabs implementation. This matches our implementation but + // I have doubts around its correctness. + let deployment_result = tracing::info_span!( + "Deploying contracts", + contract_name = input.instance + ) + .in_scope(|| { + if let Err(error) = leader_state.deploy_contracts(input, self.leader_node) { + tracing::error!(target = ?Target::Leader, ?error, "Contract deployment failed"); + execution_result.add_failed_case( + Target::Leader, + mode.clone(), + case.name.clone().unwrap_or("no case name".to_owned()), + case_idx, + input_idx, + anyhow::Error::msg( + format!("Failed to deploy contracts, {error}") + ) + ); + return Err(error) + }; + if let Err(error) = + follower_state.deploy_contracts(input, self.follower_node) + { + tracing::error!(target = ?Target::Follower, ?error, "Contract deployment failed"); + execution_result.add_failed_case( + Target::Follower, + mode.clone(), + case.name.clone().unwrap_or("no case name".to_owned()), + case_idx, + input_idx, + anyhow::Error::msg( + format!("Failed to deploy contracts, {error}") + ) + ); + return Err(error) + }; + Ok(()) + }); + if deployment_result.is_err() { + // Noting it again here: if something in the input fails we do not move on + // to the next input, we move to the next case completely. + continue 'case_loop; } - if let Err(err) = follower_state.deploy_contracts(input, self.follower_node) { - tracing::error!("Follower deployment failed for {}: {err}", input.instance); - continue; - } else { - tracing::debug!("Follower deployment succeeded for {}", &input.instance); - } + let execution_result = + tracing::info_span!("Executing input", contract_name = input.instance) + .in_scope(|| { + let (leader_receipt, _, leader_diff) = + match leader_state.execute_input(input, self.leader_node) { + Ok(result) => result, + Err(error) => { + tracing::error!( + target = ?Target::Leader, + ?error, + "Contract execution failed" + ); + return Err(error); + } + }; - tracing::debug!("Starting executing contract {}", &input.instance); + let (follower_receipt, _, follower_diff) = + match follower_state.execute_input(input, self.follower_node) { + Ok(result) => result, + Err(error) => { + tracing::error!( + target = ?Target::Follower, + ?error, + "Contract execution failed" + ); + return Err(error); + } + }; - let (leader_receipt, _, leader_diff) = - match leader_state.execute_input(input, self.leader_node) { - Ok(result) => result, - Err(err) => { - tracing::error!( - "Leader execution failed for {}: {err}", - input.instance - ); - continue; - } - }; - - let (follower_receipt, _, follower_diff) = - match follower_state.execute_input(input, self.follower_node) { - Ok(result) => result, - Err(err) => { - tracing::error!( - "Follower execution failed for {}: {err}", - input.instance - ); - continue; - } - }; + Ok((leader_receipt, leader_diff, follower_receipt, follower_diff)) + }); + let Ok((leader_receipt, leader_diff, follower_receipt, follower_diff)) = + execution_result + else { + // Noting it again here: if something in the input fails we do not move on + // to the next input, we move to the next case completely. + continue 'case_loop; + }; if leader_diff == follower_diff { tracing::debug!("State diffs match between leader and follower."); @@ -487,10 +598,238 @@ where ); } } + + // Note: Only consider the case as having been successful after we have processed + // all of the inputs and completed the entire loop over the input. + execution_result.add_successful_case( + Target::Leader, + mode.clone(), + case.name.clone().unwrap_or("no case name".to_owned()), + case_idx, + ); + execution_result.add_successful_case( + Target::Follower, + mode.clone(), + case.name.clone().unwrap_or("no case name".to_owned()), + case_idx, + ); } } - Ok(()) + execution_result + } +} + +#[derive(Debug, Default)] +pub struct ExecutionResult { + pub results: Vec>, + pub successful_cases_count: usize, + pub failed_cases_count: usize, +} + +impl ExecutionResult { + pub fn new() -> Self { + Self { + results: Default::default(), + successful_cases_count: Default::default(), + failed_cases_count: Default::default(), + } + } + + pub fn add_successful_build(&mut self, target: Target, solc_mode: SolcMode) { + self.results + .push(Box::new(BuildResult::Success { target, solc_mode })); + } + + pub fn add_failed_build(&mut self, target: Target, solc_mode: SolcMode, error: anyhow::Error) { + self.results.push(Box::new(BuildResult::Failure { + target, + solc_mode, + error, + })); + } + + pub fn add_successful_case( + &mut self, + target: Target, + solc_mode: SolcMode, + case_name: String, + case_idx: usize, + ) { + self.successful_cases_count += 1; + self.results.push(Box::new(CaseResult::Success { + target, + solc_mode, + case_name, + case_idx, + })); + } + + pub fn add_failed_case( + &mut self, + target: Target, + solc_mode: SolcMode, + case_name: String, + case_idx: usize, + input_idx: usize, + error: anyhow::Error, + ) { + self.failed_cases_count += 1; + self.results.push(Box::new(CaseResult::Failure { + target, + solc_mode, + case_name, + case_idx, + error, + input_idx, + })); + } +} + +pub trait ExecutionResultItem: Debug { + /// Converts this result item into an [`anyhow::Result`]. + fn as_result(&self) -> Result<(), &anyhow::Error>; + + /// Provides information on whether the provided result item is of a success or failure. + fn is_success(&self) -> bool; + + /// Provides information of the target that this result is for. + fn target(&self) -> &Target; + + /// Provides information on the [`SolcMode`] mode that we being used for this result item. + fn solc_mode(&self) -> &SolcMode; + + /// Provides information on the case name and number that this result item pertains to. This is + /// [`None`] if the error doesn't belong to any case (e.g., if it's a build error outside of any + /// of the cases.). + fn case_name_and_index(&self) -> Option<(&str, usize)>; + + /// Provides information on the input number that this result item pertains to. This is [`None`] + /// if the error doesn't belong to any input (e.g., if it's a build error outside of any of the + /// inputs.). + fn input_index(&self) -> Option; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Target { + Leader, + Follower, +} + +#[derive(Debug)] +pub enum BuildResult { + Success { + target: Target, + solc_mode: SolcMode, + }, + Failure { + target: Target, + solc_mode: SolcMode, + error: anyhow::Error, + }, +} + +impl ExecutionResultItem for BuildResult { + fn as_result(&self) -> Result<(), &anyhow::Error> { + match self { + Self::Success { .. } => Ok(()), + Self::Failure { error, .. } => Err(error)?, + } + } + + fn is_success(&self) -> bool { + match self { + Self::Success { .. } => true, + Self::Failure { .. } => false, + } + } + + fn target(&self) -> &Target { + match self { + Self::Success { target, .. } | Self::Failure { target, .. } => target, + } + } + + fn solc_mode(&self) -> &SolcMode { + match self { + Self::Success { solc_mode, .. } | Self::Failure { solc_mode, .. } => solc_mode, + } + } + + fn case_name_and_index(&self) -> Option<(&str, usize)> { + None + } + + fn input_index(&self) -> Option { + None + } +} + +#[derive(Debug)] +pub enum CaseResult { + Success { + target: Target, + solc_mode: SolcMode, + case_name: String, + case_idx: usize, + }, + Failure { + target: Target, + solc_mode: SolcMode, + case_name: String, + case_idx: usize, + input_idx: usize, + error: anyhow::Error, + }, +} + +impl ExecutionResultItem for CaseResult { + fn as_result(&self) -> Result<(), &anyhow::Error> { + match self { + Self::Success { .. } => Ok(()), + Self::Failure { error, .. } => Err(error)?, + } + } + + fn is_success(&self) -> bool { + match self { + Self::Success { .. } => true, + Self::Failure { .. } => false, + } + } + + fn target(&self) -> &Target { + match self { + Self::Success { target, .. } | Self::Failure { target, .. } => target, + } + } + + fn solc_mode(&self) -> &SolcMode { + match self { + Self::Success { solc_mode, .. } | Self::Failure { solc_mode, .. } => solc_mode, + } + } + + fn case_name_and_index(&self) -> Option<(&str, usize)> { + match self { + Self::Success { + case_name, + case_idx, + .. + } + | Self::Failure { + case_name, + case_idx, + .. + } => Some((case_name, *case_idx)), + } + } + + fn input_index(&self) -> Option { + match self { + CaseResult::Success { .. } => None, + CaseResult::Failure { input_idx, .. } => Some(*input_idx), + } } } diff --git a/crates/core/src/main.rs b/crates/core/src/main.rs index ee871ff..c8403af 100644 --- a/crates/core/src/main.rs +++ b/crates/core/src/main.rs @@ -13,7 +13,7 @@ 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, fmt::format::FmtSpan}; +use tracing_subscriber::{EnvFilter, FmtSubscriber}; static TEMP_DIR: LazyLock = LazyLock::new(|| TempDir::new().unwrap()); @@ -39,7 +39,7 @@ fn init_cli() -> anyhow::Result { .with_thread_ids(true) .with_thread_names(true) .with_env_filter(EnvFilter::from_default_env()) - .with_span_events(FmtSpan::ENTER | FmtSpan::CLOSE) + .with_ansi(false) .pretty() .finish(); tracing::subscriber::set_global_default(subscriber)?; @@ -116,20 +116,25 @@ where follower_nodes.round_robbin(), ); - match driver.execute(span) { - Ok(_) => { - tracing::info!( - "metadata {} success", - metadata.directory().as_ref().unwrap().display() - ); - } - Err(error) => { - tracing::warn!( - "metadata {} failure: {error:?}", - metadata.file_path.as_ref().unwrap().display() - ); + 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"); + } }, );