From 799f07f04fc861d207cddf9c857a581e795a3d0c Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Wed, 23 Jul 2025 09:54:58 +0300 Subject: [PATCH 1/4] Fix size_requirement underflow --- crates/format/src/input.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index 689ca23..e031cd2 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -159,7 +159,11 @@ impl Calldata { pub fn size_requirement(&self) -> usize { match self { - Calldata::Single(single) => (single.len() - 2) / 2, + Calldata::Single(single) => single + .len() + .checked_sub(2) + .and_then(|value| value.checked_div(2)) + .unwrap_or_default(), Calldata::Compound(items) => items.len() * 32, } } From ce1b4862b44f54677f4ed94809c3a7e726b2cc9a Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Wed, 23 Jul 2025 10:05:07 +0300 Subject: [PATCH 2/4] Add support for wildcards in exceptions --- crates/core/src/driver/mod.rs | 62 +++++++++++++++++++++-------------- crates/format/src/input.rs | 41 ++++++++++++++++++----- 2 files changed, 70 insertions(+), 33 deletions(-) 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 { From 94afdbecc79e8df9fa7440f0f4363391ba9eb07e Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Wed, 23 Jul 2025 10:13:31 +0300 Subject: [PATCH 3/4] Fix calldata construction of single calldata --- crates/format/src/input.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index f185432..44a9602 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -136,8 +136,8 @@ impl Calldata { chain_state_provider: &impl EthereumNode, ) -> anyhow::Result<()> { match self { - Calldata::Single(string) => { - alloy::hex::decode_to_slice(string, buffer)?; + Calldata::Single(bytes) => { + buffer.extend_from_slice(bytes); } Calldata::Compound(items) => { for (arg_idx, arg) in items.iter().enumerate() { From 404d2f7452cec267ac217312799e0b428586c561 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Wed, 23 Jul 2025 14:53:19 +0300 Subject: [PATCH 4/4] Better handling for length in equivalency checks --- crates/format/src/input.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index 44a9602..5ea2acb 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -182,8 +182,16 @@ impl Calldata { continue; } + let other = if other.len() < 32 { + let mut vec = other.to_vec(); + vec.resize(32, 0); + std::borrow::Cow::Owned(vec) + } else { + std::borrow::Cow::Borrowed(other) + }; + let this = resolve_argument(this, deployed_contracts, chain_state_provider)?; - let other = U256::from_be_slice(other); + let other = U256::from_be_slice(&other); if this != other { return Ok(false); }