use std::{ collections::{BTreeMap, HashMap}, sync::Arc, }; use alloy::{ consensus::EMPTY_ROOT_HASH, hex, json_abi::JsonAbi, network::{Ethereum, TransactionBuilder}, primitives::{Address, TxHash, U256}, rpc::types::{ TransactionReceipt, TransactionRequest, trace::geth::{ CallFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType, GethDebugTracingOptions, }, }, }; use anyhow::{Context as _, Result, bail}; use futures::TryStreamExt; use indexmap::IndexMap; use revive_dt_common::types::{PlatformIdentifier, PrivateKeyAllocator}; use revive_dt_format::{ metadata::{ContractInstance, ContractPathAndIdent}, steps::{ AllocateAccountStep, BalanceAssertionStep, Calldata, EtherValue, Expected, ExpectedOutput, FunctionCallStep, Method, RepeatStep, Step, StepAddress, StepIdx, StepPath, StorageEmptyAssertionStep, }, traits::ResolutionContext, }; use tokio::sync::Mutex; use tracing::{error, info, instrument}; use crate::{ differential_tests::ExecutionState, helpers::{CachedCompiler, TestDefinition, TestPlatformInformation}, }; type StepsIterator = std::vec::IntoIter<(StepPath, Step)>; pub struct Driver<'a, I> { /// The drivers for the various platforms that we're executing the tests on. platform_drivers: BTreeMap>, } impl<'a, I> Driver<'a, I> where I: Iterator {} impl<'a> Driver<'a, StepsIterator> { // region:Constructors pub async fn new_root( test_definition: &'a TestDefinition<'a>, private_key_allocator: Arc>, cached_compiler: &CachedCompiler<'a>, ) -> Result { let platform_drivers = futures::future::try_join_all(test_definition.platforms.iter().map( |(identifier, information)| { let identifier = *identifier; let private_key_allocator = private_key_allocator.clone(); async move { Self::create_platform_driver( identifier, information, test_definition, private_key_allocator, cached_compiler, ) .await .map(|driver| (identifier, driver)) } }, )) .await .context("Failed to create the drivers for the various platforms")? .into_iter() .collect::>(); Ok(Self { platform_drivers }) } async fn create_platform_driver( identifier: PlatformIdentifier, information: &'a TestPlatformInformation<'a>, test_definition: &'a TestDefinition<'a>, private_key_allocator: Arc>, cached_compiler: &CachedCompiler<'a>, ) -> Result> { let steps: Vec<(StepPath, Step)> = test_definition .case .steps_iterator() .enumerate() .map(|(step_idx, step)| -> (StepPath, Step) { (StepPath::new(vec![StepIdx::new(step_idx)]), step) }) .collect(); let steps_iterator: StepsIterator = steps.into_iter(); PlatformDriver::new( information, test_definition, private_key_allocator, cached_compiler, steps_iterator, ) .await .context(format!("Failed to create driver for {identifier}")) } // endregion:Constructors // region:Execution pub async fn execute_all(mut self) -> Result { let platform_drivers = std::mem::take(&mut self.platform_drivers); let results = futures::future::try_join_all( platform_drivers .into_values() .map(|driver| driver.execute_all()), ) .await .context("Failed to execute all of the steps on the driver")?; Ok(results.first().copied().unwrap_or_default()) } // endregion:Execution } /// The differential tests driver for a single platform. pub struct PlatformDriver<'a, I> { /// The information of the platform that this driver is for. platform_information: &'a TestPlatformInformation<'a>, /// The definition of the test that the driver is instructed to execute. test_definition: &'a TestDefinition<'a>, /// The private key allocator used by this driver and other drivers when account allocations are /// needed. private_key_allocator: Arc>, /// The execution state associated with the platform. execution_state: ExecutionState, /// The number of steps that were executed on the driver. steps_executed: usize, /// This is the queue of steps that are to be executed by the driver for this test case. Each /// time `execute_step` is called one of the steps is executed. steps_iterator: I, } impl<'a, I> PlatformDriver<'a, I> where I: Iterator, { // region:Constructors & Initialization pub async fn new( platform_information: &'a TestPlatformInformation<'a>, test_definition: &'a TestDefinition<'a>, private_key_allocator: Arc>, cached_compiler: &CachedCompiler<'a>, steps: I, ) -> Result { let execution_state = Self::init_execution_state(platform_information, test_definition, cached_compiler) .await .context("Failed to initialize the execution state of the platform")?; Ok(PlatformDriver { platform_information, test_definition, private_key_allocator, execution_state, steps_executed: 0, steps_iterator: steps, }) } async fn init_execution_state( platform_information: &'a TestPlatformInformation<'a>, test_definition: &'a TestDefinition<'a>, cached_compiler: &CachedCompiler<'a>, ) -> Result { let compiler_output = cached_compiler .compile_contracts( test_definition.metadata, test_definition.metadata_file_path, test_definition.mode.clone(), None, platform_information.compiler.as_ref(), platform_information.platform, &platform_information.reporter, ) .await .inspect_err(|err| { error!( ?err, platform_identifier = %platform_information.platform.platform_identifier(), "Pre-linking compilation failed" ) }) .context("Failed to produce the pre-linking compiled contracts")?; let mut deployed_libraries = None::>; let mut contract_sources = test_definition .metadata .contract_sources() .inspect_err(|err| { error!( ?err, platform_identifier = %platform_information.platform.platform_identifier(), "Failed to retrieve contract sources from metadata" ) }) .context("Failed to get the contract instances from the metadata file")?; for library_instance in test_definition .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 get the contract sources of the contract instance")?; let (code, abi) = compiler_output .contracts .get(&library_source_path) .and_then(|contracts| contracts.get(library_ident.as_str())) .context("Failed to get the code and abi for the instance")?; let code = alloy::hex::decode(code)?; // 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 = test_definition .case .steps .iter() .filter_map(|step| match step { Step::FunctionCall(input) => input.caller.as_address().copied(), Step::BalanceAssertion(..) => None, Step::StorageEmptyAssertion(..) => None, Step::Repeat(..) => None, Step::AllocateAccount(..) => None, }) .next() .unwrap_or(FunctionCallStep::default_caller_address()); let tx = TransactionBuilder::::with_deploy_code( TransactionRequest::default().from(deployer_address), code, ); let receipt = platform_information .node .execute_transaction(tx) .await .inspect_err(|err| { error!( ?err, %library_instance, platform_identifier = %platform_information.platform.platform_identifier(), "Failed to deploy the library" ) })?; let library_address = receipt .contract_address .expect("Failed to deploy the library"); deployed_libraries.get_or_insert_default().insert( library_instance.clone(), (library_ident.clone(), library_address, abi.clone()), ); } let compiler_output = cached_compiler .compile_contracts( test_definition.metadata, test_definition.metadata_file_path, test_definition.mode.clone(), deployed_libraries.as_ref(), platform_information.compiler.as_ref(), platform_information.platform, &platform_information.reporter, ) .await .inspect_err(|err| { error!( ?err, platform_identifier = %platform_information.platform.platform_identifier(), "Pre-linking compilation failed" ) }) .context("Failed to compile the post-link contracts")?; Ok(ExecutionState::new( compiler_output.contracts, deployed_libraries.unwrap_or_default(), )) } // endregion:Constructors & Initialization // region:Step Handling pub async fn execute_all(mut self) -> Result { while let Some(result) = self.execute_next_step().await { result? } Ok(self.steps_executed) } pub async fn execute_next_step(&mut self) -> Option> { let (step_path, step) = self.steps_iterator.next()?; info!(%step_path, "Executing Step"); Some( self.execute_step(&step_path, &step) .await .inspect(|_| info!(%step_path, "Step execution succeeded")) .inspect_err(|err| error!(%step_path, ?err, "Step execution failed")), ) } #[instrument( level = "info", skip_all, fields( platform_identifier = %self.platform_information.platform.platform_identifier(), node_id = self.platform_information.node.id(), %step_path, ), err(Debug), )] async fn execute_step(&mut self, step_path: &StepPath, step: &Step) -> Result<()> { let steps_executed = match step { Step::FunctionCall(step) => self .execute_function_call(step_path, step.as_ref()) .await .context("Function call step Failed"), Step::BalanceAssertion(step) => self .execute_balance_assertion(step_path, step.as_ref()) .await .context("Balance Assertion Step Failed"), Step::StorageEmptyAssertion(step) => self .execute_storage_empty_assertion_step(step_path, step.as_ref()) .await .context("Storage Empty Assertion Step Failed"), Step::Repeat(step) => self .execute_repeat_step(step_path, step.as_ref()) .await .context("Repetition Step Failed"), Step::AllocateAccount(step) => self .execute_account_allocation(step_path, step.as_ref()) .await .context("Account Allocation Step Failed"), } .context(format!("Failure on step {step_path}"))?; self.steps_executed += steps_executed; Ok(()) } #[instrument(level = "info", skip_all)] pub async fn execute_function_call( &mut self, _: &StepPath, step: &FunctionCallStep, ) -> Result { let deployment_receipts = self .handle_function_call_contract_deployment(step) .await .context("Failed to deploy contracts for the function call step")?; let execution_receipt = self .handle_function_call_execution(step, deployment_receipts) .await .context("Failed to handle the function call execution")?; let tracing_result = self .handle_function_call_call_frame_tracing(execution_receipt.transaction_hash) .await .context("Failed to handle the function call call frame tracing")?; self.handle_function_call_variable_assignment(step, &tracing_result) .await .context("Failed to handle function call variable assignment")?; self.handle_function_call_assertions(step, &execution_receipt, &tracing_result) .await .context("Failed to handle function call assertions")?; Ok(1) } #[instrument(level = "debug", skip_all)] async fn handle_function_call_contract_deployment( &mut self, step: &FunctionCallStep, ) -> Result> { let mut instances_we_must_deploy = IndexMap::::new(); for instance in step.find_all_contract_instances().into_iter() { if !self .execution_state .deployed_contracts .contains_key(&instance) { instances_we_must_deploy.entry(instance).or_insert(false); } } if let Method::Deployer = step.method { instances_we_must_deploy.swap_remove(&step.instance); instances_we_must_deploy.insert(step.instance.clone(), true); } let mut receipts = HashMap::new(); for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() { let calldata = deploy_with_constructor_arguments.then_some(&step.calldata); let value = deploy_with_constructor_arguments .then_some(step.value) .flatten(); let caller = { let context = self.default_resolution_context(); let resolver = self.platform_information.node.resolver().await?; step.caller .resolve_address(resolver.as_ref(), context) .await? }; if let (_, _, Some(receipt)) = self .get_or_deploy_contract_instance(&instance, caller, calldata, value) .await .context("Failed to get or deploy contract instance during input execution")? { receipts.insert(instance.clone(), receipt); } } Ok(receipts) } #[instrument(level = "debug", skip_all)] async fn handle_function_call_execution( &mut self, step: &FunctionCallStep, mut deployment_receipts: HashMap, ) -> Result { match step.method { // This step was already executed when `handle_step` was called. We just need to // lookup the transaction receipt in this case and continue on. Method::Deployer => deployment_receipts .remove(&step.instance) .context("Failed to find deployment receipt for constructor call"), Method::Fallback | Method::FunctionName(_) => { let resolver = self.platform_information.node.resolver().await?; let tx = match step .as_transaction(resolver.as_ref(), self.default_resolution_context()) .await { Ok(tx) => tx, Err(err) => { return Err(err); } }; self.platform_information.node.execute_transaction(tx).await } } } #[instrument(level = "debug", skip_all)] async fn handle_function_call_call_frame_tracing( &mut self, tx_hash: TxHash, ) -> Result { self.platform_information .node .trace_transaction( tx_hash, GethDebugTracingOptions { tracer: Some(GethDebugTracerType::BuiltInTracer( GethDebugBuiltInTracerType::CallTracer, )), tracer_config: GethDebugTracerConfig(serde_json::json! {{ "onlyTopCall": true, "withLog": false, "withStorage": false, "withMemory": false, "withStack": false, "withReturnData": true }}), ..Default::default() }, ) .await .map(|trace| { trace .try_into_call_frame() .expect("Impossible - we requested a callframe trace so we must get it back") }) } #[instrument(level = "debug", skip_all)] async fn handle_function_call_variable_assignment( &mut self, step: &FunctionCallStep, tracing_result: &CallFrame, ) -> Result<()> { let Some(ref assignments) = step.variable_assignments else { return Ok(()); }; // Handling the return data variable assignments. for (variable_name, output_word) in assignments.return_data.iter().zip( tracing_result .output .as_ref() .unwrap_or_default() .to_vec() .chunks(32), ) { let value = U256::from_be_slice(output_word); self.execution_state .variables .insert(variable_name.clone(), value); tracing::info!( variable_name, variable_value = hex::encode(value.to_be_bytes::<32>()), "Assigned variable" ); } Ok(()) } #[instrument(level = "debug", skip_all)] async fn handle_function_call_assertions( &mut self, step: &FunctionCallStep, receipt: &TransactionReceipt, tracing_result: &CallFrame, ) -> Result<()> { // Resolving the `step.expected` into a series of expectations that we can then assert on. let mut expectations = match step { FunctionCallStep { expected: Some(Expected::Calldata(calldata)), .. } => vec![ExpectedOutput::new().with_calldata(calldata.clone())], FunctionCallStep { expected: Some(Expected::Expected(expected)), .. } => vec![expected.clone()], FunctionCallStep { expected: Some(Expected::ExpectedMany(expected)), .. } => expected.clone(), FunctionCallStep { expected: None, .. } => vec![ExpectedOutput::new().with_success()], }; // This is a bit of a special case and we have to support it separately on it's own. If it's // a call to the deployer method, then the tests will assert that it "returns" the address // of the contract. Deployments do not return the address of the contract but the runtime // code of the contracts. Therefore, this assertion would always fail. So, we replace it // with an assertion of "check if it succeeded" if let Method::Deployer = &step.method { for expectation in expectations.iter_mut() { expectation.return_data = None; } } futures::stream::iter(expectations.into_iter().map(Ok)) .try_for_each_concurrent(None, |expectation| async { self.handle_function_call_assertion_item(receipt, tracing_result, expectation) .await }) .await } #[instrument(level = "debug", skip_all)] async fn handle_function_call_assertion_item( &self, receipt: &TransactionReceipt, tracing_result: &CallFrame, assertion: ExpectedOutput, ) -> Result<()> { let resolver = self .platform_information .node .resolver() .await .context("Failed to create the resolver for the node")?; if let Some(ref version_requirement) = assertion.compiler_version { if !version_requirement.matches(self.platform_information.compiler.version()) { return Ok(()); } } let resolution_context = self .default_resolution_context() .with_block_number(receipt.block_number.as_ref()) .with_transaction_hash(&receipt.transaction_hash); // Handling the receipt state assertion. let expected = !assertion.exception; let actual = receipt.status(); if actual != expected { let revert_reason = tracing_result .revert_reason .as_ref() .or(tracing_result.error.as_ref()); tracing::error!( expected, actual, ?receipt, ?tracing_result, ?revert_reason, "Transaction status assertion failed" ); anyhow::bail!( "Transaction status assertion failed - Expected {expected} but got {actual}. Revert reason: {revert_reason:?}", ); } // Handling the calldata assertion if let Some(ref expected_calldata) = assertion.return_data { let expected = expected_calldata; let actual = &tracing_result.output.as_ref().unwrap_or_default(); if !expected .is_equivalent(actual, resolver.as_ref(), resolution_context) .await .context("Failed to resolve calldata equivalence for return data assertion")? { tracing::error!( ?receipt, ?expected, %actual, "Calldata assertion failed" ); anyhow::bail!("Calldata assertion failed - Expected {expected:?} but got {actual}",); } } // Handling the events assertion if let Some(ref expected_events) = assertion.events { // Handling the events length assertion. let expected = expected_events.len(); let actual = receipt.logs().len(); if actual != expected { tracing::error!(expected, actual, "Event count assertion failed",); anyhow::bail!( "Event count assertion failed - Expected {expected} but got {actual}", ); } // Handling the events assertion. for (event_idx, (expected_event, actual_event)) in expected_events.iter().zip(receipt.logs()).enumerate() { // Handling the emitter assertion. if let Some(ref expected_address) = expected_event.address { let expected = expected_address .resolve_address(resolver.as_ref(), resolution_context) .await?; let actual = actual_event.address(); if actual != expected { tracing::error!( event_idx, %expected, %actual, "Event emitter assertion failed", ); anyhow::bail!( "Event emitter assertion failed - Expected {expected} but got {actual}", ); } } // Handling the topics assertion. for (expected, actual) in expected_event .topics .as_slice() .iter() .zip(actual_event.topics()) { let expected = Calldata::new_compound([expected]); if !expected .is_equivalent(&actual.0, resolver.as_ref(), resolution_context) .await .context("Failed to resolve event topic equivalence")? { tracing::error!( event_idx, ?receipt, ?expected, ?actual, "Event topics assertion failed", ); anyhow::bail!( "Event topics assertion failed - Expected {expected:?} but got {actual:?}", ); } } // Handling the values assertion. let expected = &expected_event.values; let actual = &actual_event.data().data; if !expected .is_equivalent(&actual.0, resolver.as_ref(), resolution_context) .await .context("Failed to resolve event value equivalence")? { tracing::error!( event_idx, ?receipt, ?expected, ?actual, "Event value assertion failed", ); anyhow::bail!( "Event value assertion failed - Expected {expected:?} but got {actual:?}", ); } } } Ok(()) } #[instrument(level = "info", skip_all)] pub async fn execute_balance_assertion( &mut self, _: &StepPath, step: &BalanceAssertionStep, ) -> anyhow::Result { self.step_address_auto_deployment(&step.address) .await .context("Failed to perform auto-deployment for the step address")?; let resolver = self.platform_information.node.resolver().await?; let address = step .address .resolve_address(resolver.as_ref(), self.default_resolution_context()) .await?; let balance = self.platform_information.node.balance_of(address).await?; let expected = step.expected_balance; let actual = balance; if expected != actual { tracing::error!(%expected, %actual, %address, "Balance assertion failed"); anyhow::bail!( "Balance assertion failed - Expected {} but got {} for {} resolved to {}", expected, actual, address, address, ) } Ok(1) } #[instrument(level = "info", skip_all, err(Debug))] async fn execute_storage_empty_assertion_step( &mut self, _: &StepPath, step: &StorageEmptyAssertionStep, ) -> Result { self.step_address_auto_deployment(&step.address) .await .context("Failed to perform auto-deployment for the step address")?; let resolver = self.platform_information.node.resolver().await?; let address = step .address .resolve_address(resolver.as_ref(), self.default_resolution_context()) .await?; let storage = self .platform_information .node .latest_state_proof(address, Default::default()) .await?; let is_empty = storage.storage_hash == EMPTY_ROOT_HASH; let expected = step.is_storage_empty; let actual = is_empty; if expected != actual { tracing::error!(%expected, %actual, %address, "Storage Empty Assertion failed"); anyhow::bail!( "Storage Empty Assertion failed - Expected {} but got {} for {} resolved to {}", expected, actual, address, address, ) }; Ok(1) } #[instrument(level = "info", skip_all, err(Debug))] async fn execute_repeat_step( &mut self, step_path: &StepPath, step: &RepeatStep, ) -> Result { let tasks = (0..step.repeat) .map(|_| PlatformDriver { platform_information: self.platform_information, test_definition: self.test_definition, private_key_allocator: self.private_key_allocator.clone(), execution_state: self.execution_state.clone(), steps_executed: 0, steps_iterator: { let steps: Vec<(StepPath, Step)> = step .steps .iter() .cloned() .enumerate() .map(|(step_idx, step)| { let step_idx = StepIdx::new(step_idx); let step_path = step_path.append(step_idx); (step_path, step) }) .collect(); steps.into_iter() }, }) .map(|driver| driver.execute_all()) .collect::>(); let res = futures::future::try_join_all(tasks) .await .context("Repetition execution failed")?; Ok(res.first().copied().unwrap_or_default()) } #[instrument(level = "info", skip_all, err(Debug))] pub async fn execute_account_allocation( &mut self, _: &StepPath, step: &AllocateAccountStep, ) -> Result { let Some(variable_name) = step.variable_name.strip_prefix("$VARIABLE:") else { bail!("Account allocation must start with $VARIABLE:"); }; let private_key = self.private_key_allocator.lock().await.allocate()?; let account = private_key.address(); let variable = U256::from_be_slice(account.0.as_slice()); self.execution_state .variables .insert(variable_name.to_string(), variable); Ok(1) } // endregion:Step Handling // region:Contract Deployment #[instrument( level = "info", skip_all, fields( %contract_instance, %deployer ), err(Debug), )] async fn get_or_deploy_contract_instance( &mut self, contract_instance: &ContractInstance, deployer: Address, calldata: Option<&Calldata>, value: Option, ) -> Result<(Address, JsonAbi, Option)> { if let Some((_, address, abi)) = self .execution_state .deployed_contracts .get(contract_instance) { info!( %address, "Contract instance already deployed." ); Ok((*address, abi.clone(), None)) } else { info!("Contract instance requires deployment."); let (address, abi, receipt) = self .deploy_contract(contract_instance, deployer, calldata, value) .await .context("Failed to deploy contract")?; info!( %address, "Contract instance has been deployed." ); Ok((address, abi, Some(receipt))) } } #[instrument( level = "info", skip_all, fields( %contract_instance, %deployer ), err(Debug), )] async fn deploy_contract( &mut self, contract_instance: &ContractInstance, deployer: Address, calldata: Option<&Calldata>, value: Option, ) -> Result<(Address, JsonAbi, TransactionReceipt)> { let Some(ContractPathAndIdent { contract_source_path, contract_ident, }) = self .test_definition .metadata .contract_sources()? .remove(contract_instance) else { anyhow::bail!( "Contract source not found for instance {:?}", contract_instance ) }; let Some((code, abi)) = self .execution_state .compiled_contracts .get(&contract_source_path) .and_then(|source_file_contracts| source_file_contracts.get(contract_ident.as_ref())) .cloned() else { anyhow::bail!( "Failed to find information for contract {:?}", contract_instance ) }; let mut code = match alloy::hex::decode(&code) { Ok(code) => code, Err(error) => { tracing::error!( ?error, contract_source_path = contract_source_path.display().to_string(), contract_ident = contract_ident.as_ref(), "Failed to hex-decode byte code - This could possibly mean that the bytecode requires linking" ); anyhow::bail!("Failed to hex-decode the byte code {}", error) } }; if let Some(calldata) = calldata { let resolver = self.platform_information.node.resolver().await?; let calldata = calldata .calldata(resolver.as_ref(), self.default_resolution_context()) .await?; code.extend(calldata); } let tx = { let tx = TransactionRequest::default().from(deployer); let tx = match value { Some(ref value) => tx.value(value.into_inner()), _ => tx, }; TransactionBuilder::::with_deploy_code(tx, code) }; let receipt = match self.platform_information.node.execute_transaction(tx).await { Ok(receipt) => receipt, Err(error) => { tracing::error!(?error, "Contract deployment transaction failed."); return Err(error); } }; let Some(address) = receipt.contract_address else { anyhow::bail!("Contract deployment didn't return an address"); }; tracing::info!( instance_name = ?contract_instance, instance_address = ?address, "Deployed contract" ); self.platform_information .reporter .report_contract_deployed_event(contract_instance.clone(), address)?; self.execution_state.deployed_contracts.insert( contract_instance.clone(), (contract_ident, address, abi.clone()), ); Ok((address, abi, receipt)) } #[instrument(level = "info", skip_all)] async fn step_address_auto_deployment( &mut self, step_address: &StepAddress, ) -> Result
{ match step_address { StepAddress::Address(address) => Ok(*address), StepAddress::ResolvableAddress(resolvable) => { let Some(instance) = resolvable .strip_suffix(".address") .map(ContractInstance::new) else { bail!("Not an address variable"); }; self.get_or_deploy_contract_instance( &instance, FunctionCallStep::default_caller_address(), None, None, ) .await .map(|v| v.0) } } } // endregion:Contract Deployment // region:Resolution & Resolver fn default_resolution_context(&self) -> ResolutionContext<'_> { ResolutionContext::default() .with_deployed_contracts(&self.execution_state.deployed_contracts) .with_variables(&self.execution_state.variables) } // endregion:Resolution & Resolver }