diff --git a/crates/core/src/driver/mod.rs b/crates/core/src/driver/mod.rs index fb13c29..f07835d 100644 --- a/crates/core/src/driver/mod.rs +++ b/crates/core/src/driver/mod.rs @@ -5,7 +5,6 @@ use std::marker::PhantomData; use alloy::json_abi::JsonAbi; use alloy::network::{Ethereum, TransactionBuilder}; -use alloy::primitives::Bytes; use alloy::rpc::types::TransactionReceipt; use alloy::rpc::types::trace::geth::{ DefaultFrame, GethDebugTracingOptions, GethDefaultTracingOptions, GethTrace, PreStateConfig, @@ -437,6 +436,9 @@ where // Additionally, what happens if the compiler filter doesn't match? Do we consider that the // transaction should succeed? Do we just ignore the expectation? + let deployed_contracts = self.deployed_contracts.entry(case_idx).or_default(); + let chain_state_provider = node; + // Handling the receipt state assertion. let expected = !expectation.exception; let actual = execution_receipt.status(); @@ -454,13 +456,16 @@ where // Handling the calldata assertion if let Some(ref expected_calldata) = expectation.return_data { - let expected = expected_calldata - .calldata(self.deployed_contracts.entry(case_idx).or_default(), node) - .map(Bytes::from)?; - let actual = tracing_result.return_value.clone(); - if !expected.starts_with(&actual) { - tracing::error!(?execution_receipt, %expected, %actual, "Calldata assertion failed"); - anyhow::bail!("Calldata assertion failed - Expected {expected} but got {actual}",); + let expected = expected_calldata; + let actual = &tracing_result.return_value; + if !expected.is_equivalent(actual, deployed_contracts, chain_state_provider)? { + tracing::error!( + ?execution_receipt, + ?expected, + %actual, + "Calldata assertion failed" + ); + anyhow::bail!("Calldata assertion failed - Expected {expected:?} but got {actual}",); } } @@ -503,27 +508,34 @@ where } // Handling the topics assertion. - let expected = expected_event.topics.as_slice(); - let actual = actual_event.topics(); - if actual != expected { - tracing::error!( - ?execution_receipt, - ?expected, - ?actual, - "Event topics assertion failed", - ); - anyhow::bail!( - "Event topics assertion failed - Expected {expected:?} but got {actual:?}", - ); + for (expected, actual) in expected_event + .topics + .as_slice() + .iter() + .zip(actual_event.topics()) + { + let expected = Calldata::Compound(vec![expected.clone()]); + if !expected.is_equivalent( + &actual.0, + deployed_contracts, + chain_state_provider, + )? { + tracing::error!( + ?execution_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 - .calldata(self.deployed_contracts.entry(case_idx).or_default(), node) - .map(Bytes::from)?; + let expected = &expected_event.values; let actual = &actual_event.data().data; - if !expected.starts_with(actual) { + if !expected.is_equivalent(&actual.0, deployed_contracts, chain_state_provider)? { tracing::error!( ?execution_receipt, ?expected, diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index e031cd2..f185432 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -7,7 +7,6 @@ use alloy::{ primitives::{Address, Bytes, U256}, rpc::types::TransactionRequest, }; -use alloy_primitives::B256; use semver::VersionReq; use serde::Deserialize; @@ -50,14 +49,14 @@ pub struct ExpectedOutput { #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] pub struct Event { pub address: Option
, - pub topics: Vec, + pub topics: Vec, pub values: Calldata, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] #[serde(untagged)] pub enum Calldata { - Single(String), + Single(Bytes), Compound(Vec), } @@ -159,14 +158,40 @@ impl Calldata { pub fn size_requirement(&self) -> usize { match self { - Calldata::Single(single) => single - .len() - .checked_sub(2) - .and_then(|value| value.checked_div(2)) - .unwrap_or_default(), + Calldata::Single(single) => single.len(), Calldata::Compound(items) => items.len() * 32, } } + + /// Checks if this [`Calldata`] is equivalent to the passed calldata bytes. + pub fn is_equivalent( + &self, + other: &[u8], + deployed_contracts: &HashMap, + chain_state_provider: &impl EthereumNode, + ) -> anyhow::Result { + match self { + Calldata::Single(calldata) => Ok(calldata == other), + Calldata::Compound(items) => { + // Chunking the "other" calldata into 32 byte chunks since each + // one of the items in the compound calldata represents 32 bytes + for (this, other) in items.iter().zip(other.chunks(32)) { + // The matterlabs format supports wildcards and therefore we + // also need to support them. + if this == "*" { + continue; + } + + let this = resolve_argument(this, deployed_contracts, chain_state_provider)?; + let other = U256::from_be_slice(other); + if this != other { + return Ok(false); + } + } + Ok(true) + } + } + } } impl Input {