Compare commits

...

7 Commits

Author SHA1 Message Date
Omar Abdulla e77962ee66 Attempt to improve the geth tx indexing issue.
We're facing an issue where Geth transaction indexing can sometimes stall
on some of the nodes we're running. The logs show that for all transactions
we always need 1 second of waiting time. However, during certain runs we
sometimes run into an issue with some of the nodes where it seems like
their transaction indexer fails (either at the start or after some amount
of time) which leads us to never get the receipts back from these specific
nodes.

This is not a load issue as it appears like all of the other nodes handle
it just fine. However, it looks like once a node gets into this state it
can not get out of it and its bricked for the entire run.

This commit adds some more command line arguments to the geth command in
hopes of improving this issue.
2025-07-28 17:45:58 +03:00
Omar Abdulla f6aa7c9109 Allow for files to be specified in the corpus file 2025-07-28 17:45:50 +03:00
Omar 429f2e92a2 Fix contract discovery for simple tests (#83) 2025-07-28 07:05:53 +00:00
Omar 65f41f2038 Correct the type of address in matterlabs events (#82) 2025-07-28 05:01:52 +00:00
Omar 3ed8a1ca1c Support compiler-version aware exceptions (#81) 2025-07-25 14:23:17 +00:00
Omar 2923d675cd Support Compile-time Linking (#79)
* Use wrappers for libraries in metadata.

* Create a unified way to access deployed contracts

* Support linking at compile time
2025-07-25 07:03:21 +00:00
Omar 8f5bcf08ad Support Calldata arithmetic (#77)
* Re-order the input file.

This commit reorders the input file such that we have a definitions
section and an implementations section and such that the the order of
the items in both sections is the same.

* Implement a reverse polish calculator for calldata arithmetic
2025-07-24 15:35:25 +00:00
10 changed files with 985 additions and 425 deletions
Generated
+1
View File
@@ -3963,6 +3963,7 @@ dependencies = [
name = "revive-dt-compiler"
version = "0.1.0"
dependencies = [
"alloy-primitives",
"anyhow",
"revive-common",
"revive-dt-config",
@@ -48,11 +48,7 @@ macro_rules! define_wrapper_type {
$vis struct $ident($ty);
impl $ident {
pub fn new(value: $ty) -> Self {
Self(value)
}
pub fn new_from<T: Into<$ty>>(value: T) -> Self {
pub fn new(value: impl Into<$ty>) -> Self {
Self(value.into())
}
+3 -1
View File
@@ -9,11 +9,13 @@ repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow = { workspace = true }
revive-solc-json-interface = { workspace = true }
revive-dt-config = { workspace = true }
revive-dt-solc-binaries = { workspace = true }
revive-common = { workspace = true }
alloy-primitives = { workspace = true }
anyhow = { workspace = true }
semver = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
+21
View File
@@ -9,6 +9,7 @@ use std::{
path::{Path, PathBuf},
};
use alloy_primitives::Address;
use revive_dt_config::Arguments;
use revive_common::EVMVersion;
@@ -158,6 +159,26 @@ where
self
}
pub fn with_library(
mut self,
scope: impl AsRef<Path>,
library_ident: impl AsRef<str>,
library_address: Address,
) -> Self {
self.input
.settings
.libraries
.get_or_insert_with(Default::default)
.entry(scope.as_ref().display().to_string())
.or_default()
.insert(
library_ident.as_ref().to_owned(),
library_address.to_string(),
);
self
}
pub fn try_build(self, solc_path: PathBuf) -> anyhow::Result<CompilerOutput<T::Options>> {
T::new(solc_path).build(CompilerInput {
extra_options: self.extra_options,
+4
View File
@@ -10,6 +10,10 @@ use crate::{CompilerInput, CompilerOutput, SolidityCompiler};
use revive_dt_config::Arguments;
use revive_solc_json_interface::SolcStandardJsonOutput;
// TODO: I believe that we need to also pass the solc compiler to resolc so that resolc uses the
// specified solc compiler. I believe that currently we completely ignore the specified solc binary
// when invoking resolc which doesn't seem right if we're using solc as a compiler frontend.
/// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode.
#[derive(Debug)]
pub struct Resolc {
+321 -143
View File
@@ -3,6 +3,7 @@
use std::collections::HashMap;
use std::fmt::Debug;
use std::marker::PhantomData;
use std::str::FromStr;
use alloy::json_abi::JsonAbi;
use alloy::network::{Ethereum, TransactionBuilder};
@@ -26,8 +27,8 @@ use revive_dt_common::iterators::FilesWithExtensionIterator;
use revive_dt_compiler::{Compiler, SolidityCompiler};
use revive_dt_config::Arguments;
use revive_dt_format::case::CaseIdx;
use revive_dt_format::input::{Calldata, Expected, ExpectedOutput, Method};
use revive_dt_format::metadata::{ContractInstance, ContractPathAndIdentifier};
use revive_dt_format::input::{Calldata, EtherValue, Expected, ExpectedOutput, Method};
use revive_dt_format::metadata::{ContractInstance, ContractPathAndIdent};
use revive_dt_format::{input::Input, metadata::Metadata, mode::SolcMode};
use revive_dt_node::Node;
use revive_dt_node_interaction::EthereumNode;
@@ -57,6 +58,12 @@ pub struct State<'a, T: Platform> {
/// files.
deployed_contracts: HashMap<CaseIdx, HashMap<ContractInstance, (Address, JsonAbi)>>,
/// This is a map of the deployed libraries.
///
/// This map is not per case, but rather, per metadata file. This means that we do not redeploy
/// the libraries with each case.
deployed_libraries: HashMap<ContractInstance, (Address, JsonAbi)>,
phantom: PhantomData<T>,
}
@@ -70,6 +77,7 @@ where
span,
contracts: Default::default(),
deployed_contracts: Default::default(),
deployed_libraries: Default::default(),
phantom: Default::default(),
}
}
@@ -93,13 +101,49 @@ where
anyhow::bail!("unsupported solc version: {:?}", &mode.solc_version);
};
// Note: if the metadata is contained within a solidity file then this is the only file that
// we wish to compile since this is a self-contained test. Otherwise, if it's a JSON file
// then we need to compile all of the contracts that are in the directory since imports are
// allowed in there.
let Some(ref metadata_file_path) = metadata.file_path else {
anyhow::bail!("The metadata file path is not defined");
};
let mut files_to_compile = if metadata_file_path
.extension()
.is_some_and(|extension| extension.eq_ignore_ascii_case("sol"))
{
Box::new(std::iter::once(metadata_file_path.clone())) as Box<dyn Iterator<Item = _>>
} else {
Box::new(
FilesWithExtensionIterator::new(metadata.directory()?)
.with_allowed_extension("sol"),
)
};
let compiler = Compiler::<T::Compiler>::new()
.allow_path(metadata.directory()?)
.solc_optimizer(mode.solc_optimize());
let mut compiler =
files_to_compile.try_fold(compiler, |compiler, path| compiler.with_source(&path))?;
for (library_instance, (library_address, _)) in self.deployed_libraries.iter() {
let library_ident = &metadata
.contracts
.as_ref()
.and_then(|contracts| contracts.get(library_instance))
.expect("Impossible for library to not be found in contracts")
.contract_ident;
let compiler = FilesWithExtensionIterator::new(metadata.directory()?)
.with_allowed_extension("sol")
.try_fold(compiler, |compiler, path| compiler.with_source(&path))?;
// Note the following: we need to tell solc which files require the libraries to be
// linked into them. We do not have access to this information and therefore we choose
// an easier, yet more compute intensive route, of telling solc that all of the files
// need to link the library and it will only perform the linking for the files that do
// actually need the library.
compiler = FilesWithExtensionIterator::new(metadata.directory()?)
.with_allowed_extension("sol")
.fold(compiler, |compiler, path| {
compiler.with_library(&path, library_ident.as_str(), *library_address)
});
}
let mut task = CompilationTask {
json_input: compiler.input(),
@@ -141,18 +185,47 @@ where
}
}
pub fn build_and_publish_libraries(
&mut self,
metadata: &Metadata,
mode: &SolcMode,
node: &T::Blockchain,
) -> anyhow::Result<()> {
self.build_contracts(mode, metadata)?;
for library_instance in metadata
.libraries
.iter()
.flatten()
.flat_map(|(_, map)| map.values())
{
self.get_or_deploy_contract_instance(
library_instance,
metadata,
None,
Input::default_caller(),
None,
None,
node,
)?;
}
Ok(())
}
pub fn handle_input(
&mut self,
metadata: &Metadata,
case_idx: CaseIdx,
input: &Input,
node: &T::Blockchain,
mode: &SolcMode,
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
let deployment_receipts =
self.handle_contract_deployment(metadata, case_idx, input, node)?;
let execution_receipt =
self.handle_input_execution(case_idx, input, deployment_receipts, node)?;
self.handle_input_expectations(case_idx, input, &execution_receipt, node)?;
self.handle_input_expectations(case_idx, input, &execution_receipt, node, mode)?;
self.handle_input_diff(case_idx, execution_receipt, node)
}
@@ -173,12 +246,7 @@ where
let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new();
for instance in input.find_all_contract_instances().into_iter() {
if !self
.deployed_contracts
.entry(case_idx)
.or_default()
.contains_key(&instance)
{
if !self.deployed_contracts(case_idx).contains_key(&instance) {
instances_we_must_deploy.entry(instance).or_insert(false);
}
}
@@ -194,124 +262,22 @@ where
let mut receipts = HashMap::new();
for (instance, deploy_with_constructor_arguments) in instances_we_must_deploy.into_iter() {
// What we have at this moment is just a contract instance which is kind of like a variable
// name for an actual underlying contract. So, we need to resolve this instance to the info
// of the contract that it belongs to.
let Some(ContractPathAndIdentifier {
contract_source_path,
contract_ident,
}) = metadata.contract_sources()?.remove(&instance)
else {
tracing::error!("Contract source not found for instance");
anyhow::bail!("Contract source not found for instance {:?}", instance)
};
let calldata = deploy_with_constructor_arguments.then_some(&input.calldata);
let value = deploy_with_constructor_arguments
.then_some(input.value)
.flatten();
let compiled_contract = self.contracts.iter().find_map(|output| {
output
.contracts
.as_ref()?
.get(&contract_source_path.display().to_string())
.and_then(|source_file_contracts| {
source_file_contracts.get(contract_ident.as_ref())
})
});
let Some(code) = compiled_contract
.and_then(|contract| contract.evm.as_ref().and_then(|evm| evm.bytecode.as_ref()))
else {
tracing::error!(
contract_source_path = contract_source_path.display().to_string(),
contract_ident = contract_ident.as_ref(),
"Failed to find bytecode for contract"
);
anyhow::bail!("Failed to find bytecode for contract {:?}", instance)
};
// TODO: When we want to do linking it would be best to do it at this stage here. We have
// the context from the metadata files and therefore know what needs to be linked and in
// what order it needs to happen.
let mut code = match alloy::hex::decode(&code.object) {
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 deploy_with_constructor_arguments {
let encoded_input = input
.encoded_input(self.deployed_contracts.entry(case_idx).or_default(), node)?;
code.extend(encoded_input.to_vec());
if let (_, _, Some(receipt)) = self.get_or_deploy_contract_instance(
&instance,
metadata,
case_idx,
input.caller,
calldata,
value,
node,
)? {
receipts.insert(instance.clone(), receipt);
}
let tx = {
let tx = TransactionRequest::default().from(input.caller);
let tx = match input.value {
Some(ref value) if deploy_with_constructor_arguments => {
tx.value(value.into_inner())
}
_ => tx,
};
TransactionBuilder::<Ethereum>::with_deploy_code(tx, code)
};
let receipt = match node.execute_transaction(tx) {
Ok(receipt) => receipt,
Err(error) => {
tracing::error!(
node = std::any::type_name::<T>(),
?error,
"Contract deployment transaction failed."
);
return Err(error);
}
};
let Some(address) = receipt.contract_address else {
tracing::error!("Contract deployment transaction didn't return an address");
anyhow::bail!("Contract deployment didn't return an address");
};
tracing::info!(
instance_name = ?instance,
instance_address = ?address,
"Deployed contract"
);
let Some(Value::String(metadata)) =
compiled_contract.and_then(|contract| contract.metadata.as_ref())
else {
tracing::error!("Contract does not have a metadata field");
anyhow::bail!("Contract does not have a metadata field");
};
let Ok(metadata) = serde_json::from_str::<Value>(metadata) else {
tracing::error!(%metadata, "Failed to parse solc metadata into a structured value");
anyhow::bail!("Failed to parse solc metadata into a structured value {metadata}");
};
let Some(abi) = metadata.get("output").and_then(|value| value.get("abi")) else {
tracing::error!(%metadata, "Failed to access the .output.abi field of the solc metadata");
anyhow::bail!(
"Failed to access the .output.abi field of the solc metadata {metadata}"
);
};
let Ok(abi) = serde_json::from_value::<JsonAbi>(abi.clone()) else {
tracing::error!(%metadata, "Failed to deserialize ABI into a structured format");
anyhow::bail!("Failed to deserialize ABI into a structured format {metadata}");
};
self.deployed_contracts
.entry(case_idx)
.or_default()
.insert(instance.clone(), (address, abi));
receipts.insert(instance.clone(), receipt);
}
Ok(receipts)
@@ -332,9 +298,7 @@ where
.remove(&input.instance)
.context("Failed to find deployment receipt"),
Method::Fallback | Method::FunctionName(_) => {
let tx = match input
.legacy_transaction(self.deployed_contracts.entry(case_idx).or_default(), node)
{
let tx = match input.legacy_transaction(self.deployed_contracts(case_idx), node) {
Ok(tx) => {
tracing::debug!("Legacy transaction data: {tx:#?}");
tx
@@ -368,6 +332,7 @@ where
input: &Input,
execution_receipt: &TransactionReceipt,
node: &T::Blockchain,
mode: &SolcMode,
) -> anyhow::Result<()> {
let span = tracing::info_span!("Handling input expectations");
let _guard = span.enter();
@@ -423,6 +388,7 @@ where
node,
expectation,
&tracing_result,
mode,
)?;
}
@@ -436,13 +402,18 @@ where
node: &T::Blockchain,
expectation: &ExpectedOutput,
tracing_result: &CallFrame,
mode: &SolcMode,
) -> anyhow::Result<()> {
// TODO: We want to respect the compiler version filter on the expected output but would
// require some changes to the interfaces of the compiler and such. So, we add it later.
// Additionally, what happens if the compiler filter doesn't match? Do we consider that the
// transaction should succeed? Do we just ignore the expectation?
if let Some(ref version_requirement) = expectation.compiler_version {
let Some(compiler_version) = mode.last_patch_version(&self.config.solc) else {
anyhow::bail!("unsupported solc version: {:?}", &mode.solc_version);
};
if !version_requirement.matches(&compiler_version) {
return Ok(());
}
}
let deployed_contracts = self.deployed_contracts.entry(case_idx).or_default();
let deployed_contracts = self.deployed_contracts(case_idx);
let chain_state_provider = node;
// Handling the receipt state assertion.
@@ -487,8 +458,18 @@ where
expected_events.iter().zip(execution_receipt.logs())
{
// Handling the emitter assertion.
if let Some(expected_address) = expected_event.address {
let expected = expected_address;
if let Some(ref expected_address) = expected_event.address {
let expected = if let Some(contract_instance) = expected_address
.strip_suffix(".address")
.map(ContractInstance::new)
{
deployed_contracts
.get(&contract_instance)
.map(|(address, _)| *address)
} else {
Address::from_str(expected_address).ok()
}
.context("Failed to get the address of the event")?;
let actual = actual_event.address();
if actual != expected {
tracing::error!(
@@ -509,7 +490,7 @@ where
.iter()
.zip(actual_event.topics())
{
let expected = Calldata::Compound(vec![expected.clone()]);
let expected = Calldata::new_compound([expected]);
if !expected.is_equivalent(
&actual.0,
deployed_contracts,
@@ -567,6 +548,162 @@ where
Ok((execution_receipt, trace, diff))
}
fn deployed_contracts(
&mut self,
case_idx: impl Into<Option<CaseIdx>>,
) -> &mut HashMap<ContractInstance, (Address, JsonAbi)> {
match case_idx.into() {
Some(case_idx) => self
.deployed_contracts
.entry(case_idx)
.or_insert_with(|| self.deployed_libraries.clone()),
None => &mut self.deployed_libraries,
}
}
/// Gets the information of a deployed contract or library from the state. If it's found to not
/// be deployed then it will be deployed.
///
/// If a [`CaseIdx`] is not specified then this contact instance address will be stored in the
/// cross-case deployed contracts address mapping.
#[allow(clippy::too_many_arguments)]
pub fn get_or_deploy_contract_instance(
&mut self,
contract_instance: &ContractInstance,
metadata: &Metadata,
case_idx: impl Into<Option<CaseIdx>>,
deployer: Address,
calldata: Option<&Calldata>,
value: Option<EtherValue>,
node: &T::Blockchain,
) -> anyhow::Result<(Address, JsonAbi, Option<TransactionReceipt>)> {
let case_idx = case_idx.into();
if let Some((address, abi)) = self.deployed_libraries.get(contract_instance) {
return Ok((*address, abi.clone(), None));
}
if let Some(case_idx) = case_idx {
if let Some((address, abi)) = self
.deployed_contracts
.get(&case_idx)
.and_then(|contracts| contracts.get(contract_instance))
{
return Ok((*address, abi.clone(), None));
}
}
let Some(ContractPathAndIdent {
contract_source_path,
contract_ident,
}) = metadata.contract_sources()?.remove(contract_instance)
else {
tracing::error!("Contract source not found for instance");
anyhow::bail!(
"Contract source not found for instance {:?}",
contract_instance
)
};
let compiled_contract = self.contracts.iter().rev().find_map(|output| {
output
.contracts
.as_ref()?
.get(&contract_source_path.display().to_string())
.and_then(|source_file_contracts| {
source_file_contracts.get(contract_ident.as_ref())
})
});
let Some(code) = compiled_contract
.and_then(|contract| contract.evm.as_ref().and_then(|evm| evm.bytecode.as_ref()))
else {
tracing::error!(
contract_source_path = contract_source_path.display().to_string(),
contract_ident = contract_ident.as_ref(),
"Failed to find bytecode for contract"
);
anyhow::bail!(
"Failed to find bytecode for contract {:?}",
contract_instance
)
};
let mut code = match alloy::hex::decode(&code.object) {
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)
}
};
let Some(Value::String(metadata)) =
compiled_contract.and_then(|contract| contract.metadata.as_ref())
else {
tracing::error!("Contract does not have a metadata field");
anyhow::bail!("Contract does not have a metadata field");
};
let Ok(metadata) = serde_json::from_str::<Value>(metadata) else {
tracing::error!(%metadata, "Failed to parse solc metadata into a structured value");
anyhow::bail!("Failed to parse solc metadata into a structured value {metadata}");
};
let Some(abi) = metadata.get("output").and_then(|value| value.get("abi")) else {
tracing::error!(%metadata, "Failed to access the .output.abi field of the solc metadata");
anyhow::bail!("Failed to access the .output.abi field of the solc metadata {metadata}");
};
let Ok(abi) = serde_json::from_value::<JsonAbi>(abi.clone()) else {
tracing::error!(%metadata, "Failed to deserialize ABI into a structured format");
anyhow::bail!("Failed to deserialize ABI into a structured format {metadata}");
};
if let Some(calldata) = calldata {
let calldata = calldata.calldata(self.deployed_contracts(case_idx), node)?;
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::<Ethereum>::with_deploy_code(tx, code)
};
let receipt = match node.execute_transaction(tx) {
Ok(receipt) => receipt,
Err(error) => {
tracing::error!(
node = std::any::type_name::<T>(),
?error,
"Contract deployment transaction failed."
);
return Err(error);
}
};
let Some(address) = receipt.contract_address else {
tracing::error!("Contract deployment transaction didn't return an address");
anyhow::bail!("Contract deployment didn't return an address");
};
tracing::info!(
instance_name = ?contract_instance,
instance_address = ?address,
"Deployed contract"
);
self.deployed_contracts(case_idx)
.insert(contract_instance.clone(), (address, abi.clone()));
Ok((address, abi, Some(receipt)))
}
}
pub struct Driver<'a, Leader: Platform, Follower: Platform> {
@@ -673,6 +810,42 @@ where
let mut leader_state = State::<L>::new(self.config, span);
let mut follower_state = State::<F>::new(self.config, span);
// Note: we are currently forced to do two compilation passes due to linking. In the
// first compilation pass we compile the libraries and publish them to the chain. In the
// second compilation pass we compile the contracts with the library addresses so that
// they're linked at compile-time.
let build_result = tracing::info_span!("Building and publishing libraries")
.in_scope(|| {
match leader_state.build_and_publish_libraries(self.metadata, &mode, self.leader_node) {
Ok(_) => {
tracing::debug!(target = ?Target::Leader, "Library building succeeded");
execution_result.add_successful_build(Target::Leader, mode.clone());
},
Err(error) => {
tracing::error!(target = ?Target::Leader, ?error, "Library building failed");
execution_result.add_failed_build(Target::Leader, mode.clone(), error);
return Err(());
}
}
match follower_state.build_and_publish_libraries(self.metadata, &mode, self.follower_node) {
Ok(_) => {
tracing::debug!(target = ?Target::Follower, "Library building succeeded");
execution_result.add_successful_build(Target::Follower, mode.clone());
},
Err(error) => {
tracing::error!(target = ?Target::Follower, ?error, "Library 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;
}
// 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.
@@ -709,7 +882,6 @@ where
// 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",
@@ -718,7 +890,7 @@ where
);
let _guard = tracing_span.enter();
let case_idx = CaseIdx::new_from(case_idx);
let case_idx = CaseIdx::new(case_idx);
// 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).
@@ -730,8 +902,13 @@ where
tracing::info_span!("Executing input", contract_name = ?input.instance)
.in_scope(|| {
let (leader_receipt, _, leader_diff) = match leader_state
.handle_input(self.metadata, case_idx, &input, self.leader_node)
{
.handle_input(
self.metadata,
case_idx,
&input,
self.leader_node,
&mode,
) {
Ok(result) => result,
Err(error) => {
tracing::error!(
@@ -760,6 +937,7 @@ where
case_idx,
&input,
self.follower_node,
&mode,
) {
Ok(result) => result,
Err(error) => {
+34 -20
View File
@@ -35,33 +35,47 @@ impl Corpus {
///
/// `path` is expected to be a directory.
pub fn collect_metadata(path: &Path, tests: &mut Vec<MetadataFile>) {
let dir_entry = match std::fs::read_dir(path) {
Ok(dir_entry) => dir_entry,
Err(error) => {
tracing::error!("failed to read dir '{}': {error}", path.display());
return;
}
};
for entry in dir_entry {
let entry = match entry {
Ok(entry) => entry,
if path.is_dir() {
let dir_entry = match std::fs::read_dir(path) {
Ok(dir_entry) => dir_entry,
Err(error) => {
tracing::error!("error reading dir entry: {error}");
continue;
tracing::error!("failed to read dir '{}': {error}", path.display());
return;
}
};
let path = entry.path();
if path.is_dir() {
collect_metadata(&path, tests);
continue;
}
for entry in dir_entry {
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
tracing::error!("error reading dir entry: {error}");
continue;
}
};
if path.is_file() {
if let Some(metadata) = MetadataFile::try_from_file(&path) {
let path = entry.path();
if path.is_dir() {
collect_metadata(&path, tests);
continue;
}
if path.is_file() {
if let Some(metadata) = MetadataFile::try_from_file(&path) {
tests.push(metadata)
}
}
}
} else {
let Some(extension) = path.extension() else {
tracing::error!("Failed to get file extension");
return;
};
if extension.eq_ignore_ascii_case("sol") || extension.eq_ignore_ascii_case("json") {
if let Some(metadata) = MetadataFile::try_from_file(path) {
tests.push(metadata)
}
} else {
tracing::error!(?extension, "Unsupported file extension");
}
}
}
+577 -240
View File
@@ -8,6 +8,7 @@ use alloy::{
rpc::types::TransactionRequest,
};
use alloy_primitives::{FixedBytes, utils::parse_units};
use anyhow::Context;
use semver::VersionReq;
use serde::{Deserialize, Serialize};
@@ -18,10 +19,10 @@ use crate::traits::ResolverApi;
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Input {
#[serde(default = "default_caller")]
#[serde(default = "Input::default_caller")]
pub caller: Address,
pub comment: Option<String>,
#[serde(default = "default_instance")]
#[serde(default = "Input::default_instance")]
pub instance: ContractInstance,
pub method: Method,
#[serde(default)]
@@ -50,16 +51,91 @@ pub struct ExpectedOutput {
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Event {
pub address: Option<Address>,
pub address: Option<String>,
pub topics: Vec<String>,
pub values: Calldata,
}
/// A type definition for the calldata supported by the testing framework.
///
/// We choose to document all of the types used in [`Calldata`] in this one doc comment to elaborate
/// on why they exist and consolidate all of the documentation for calldata in a single place where
/// it can be viewed and understood.
///
/// The [`Single`] variant of this enum is quite simple and straightforward: it's a hex-encoded byte
/// array of the calldata.
///
/// The [`Compound`] type is more intricate and allows for capabilities such as resolution and some
/// simple arithmetic operations. It houses a vector of [`CalldataItem`]s which is just a wrapper
/// around an owned string.
///
/// A [`CalldataItem`] could be a simple hex string of a single calldata argument, but it could also
/// be something that requires resolution such as `MyContract.address` which is a variable that is
/// understood by the resolution logic to mean "Lookup the address of this particular contract
/// instance".
///
/// In addition to the above, the format supports some simple arithmetic operations like add, sub,
/// divide, multiply, bitwise AND, bitwise OR, and bitwise XOR. Our parser understands the [reverse
/// polish notation] simply because it's easy to write a calculator for that notation and since we
/// do not have plans to use arithmetic too often in tests. In reverse polish notation a typical
/// `2 + 4` would be written as `2 4 +` which makes this notation very simple to implement through
/// a stack.
///
/// Combining the above, a single [`CalldataItem`] could employ both resolution and arithmetic at
/// the same time. For example, a [`CalldataItem`] of `$BLOCK_NUMBER $BLOCK_NUMBER +` means that
/// the block number should be retrieved and then it should be added to itself.
///
/// Internally, we split the [`CalldataItem`] by spaces. Therefore, `$BLOCK_NUMBER $BLOCK_NUMBER+`
/// is invalid but `$BLOCK_NUMBER $BLOCK_NUMBER +` is valid and can be understood by the parser and
/// calculator. After the split is done, each token is parsed into a [`CalldataToken<&str>`] forming
/// an [`Iterator`] over [`CalldataToken<&str>`]. A [`CalldataToken<&str>`] can then be resolved
/// into a [`CalldataToken<U256>`] through the resolution logic. Finally, after resolution is done,
/// this iterator of [`CalldataToken<U256>`] is collapsed into the final result by applying the
/// arithmetic operations requested.
///
/// For example, supplying a [`Compound`] calldata of `0xdeadbeef` produces an iterator of a single
/// [`CalldataToken<&str>`] items of the value [`CalldataToken::Item`] of the string value 12 which
/// we can then resolve into the appropriate [`U256`] value and convert into calldata.
///
/// In summary, the various types used in [`Calldata`] represent the following:
/// - [`CalldataItem`]: A calldata string from the metadata files.
/// - [`CalldataToken<&str>`]: Typically used in an iterator of items from the space splitted
/// [`CalldataItem`] and represents a token that has not yet been resolved into its value.
/// - [`CalldataToken<U256>`]: Represents a token that's been resolved from being a string and into
/// the word-size calldata argument on which we can perform arithmetic.
///
/// [`Single`]: Calldata::Single
/// [`Compound`]: Calldata::Compound
/// [reverse polish notation]: https://en.wikipedia.org/wiki/Reverse_Polish_notation
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(untagged)]
pub enum Calldata {
Single(Bytes),
Compound(Vec<String>),
Compound(Vec<CalldataItem>),
}
define_wrapper_type! {
/// This represents an item in the [`Calldata::Compound`] variant.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CalldataItem(String);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
enum CalldataToken<T> {
Item(T),
Operation(Operation),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
enum Operation {
Addition,
Subtraction,
Multiplication,
Division,
BitwiseAnd,
BitwiseOr,
BitwiseXor,
}
/// Specify how the contract is called.
@@ -88,154 +164,17 @@ define_wrapper_type!(
pub struct EtherValue(U256);
);
impl Serialize for EtherValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
format!("{} wei", self.0).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for EtherValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let string = String::deserialize(deserializer)?;
let mut splitted = string.split(' ');
let (Some(value), Some(unit)) = (splitted.next(), splitted.next()) else {
return Err(serde::de::Error::custom("Failed to parse the value"));
};
let parsed = parse_units(value, unit.replace("eth", "ether"))
.map_err(|_| serde::de::Error::custom("Failed to parse units"))?
.into();
Ok(Self(parsed))
}
}
impl ExpectedOutput {
pub fn new() -> Self {
Default::default()
}
pub fn with_success(mut self) -> Self {
self.exception = false;
self
}
pub fn with_failure(mut self) -> Self {
self.exception = true;
self
}
pub fn with_calldata(mut self, calldata: Calldata) -> Self {
self.return_data = Some(calldata);
self
}
}
impl Default for Calldata {
fn default() -> Self {
Self::Compound(Default::default())
}
}
impl Calldata {
pub fn find_all_contract_instances(&self, vec: &mut Vec<ContractInstance>) {
if let Calldata::Compound(compound) = self {
for item in compound {
if let Some(instance) = item.strip_suffix(".address") {
vec.push(ContractInstance::new_from(instance))
}
}
}
}
pub fn calldata(
&self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<Vec<u8>> {
let mut buffer = Vec::<u8>::with_capacity(self.size_requirement());
self.calldata_into_slice(&mut buffer, deployed_contracts, chain_state_provider)?;
Ok(buffer)
}
pub fn calldata_into_slice(
&self,
buffer: &mut Vec<u8>,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<()> {
match self {
Calldata::Single(bytes) => {
buffer.extend_from_slice(bytes);
}
Calldata::Compound(items) => {
for (arg_idx, arg) in items.iter().enumerate() {
match resolve_argument(arg, deployed_contracts, chain_state_provider) {
Ok(resolved) => {
buffer.extend(resolved.to_be_bytes::<32>());
}
Err(error) => {
tracing::error!(arg, arg_idx, ?error, "Failed to resolve argument");
return Err(error);
}
};
}
}
};
Ok(())
}
pub fn size_requirement(&self) -> usize {
match self {
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<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<bool> {
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 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);
if this != other {
return Ok(false);
}
}
Ok(true)
}
}
}
}
impl Input {
pub const fn default_caller() -> Address {
Address(FixedBytes(alloy::hex!(
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
)))
}
fn default_instance() -> ContractInstance {
ContractInstance::new("Test")
}
fn instance_to_address(
&self,
instance: &ContractInstance,
@@ -343,84 +282,337 @@ impl Input {
}
}
fn default_instance() -> ContractInstance {
ContractInstance::new_from("Test")
impl ExpectedOutput {
pub fn new() -> Self {
Default::default()
}
pub fn with_success(mut self) -> Self {
self.exception = false;
self
}
pub fn with_failure(mut self) -> Self {
self.exception = true;
self
}
pub fn with_calldata(mut self, calldata: Calldata) -> Self {
self.return_data = Some(calldata);
self
}
}
pub const fn default_caller() -> Address {
Address(FixedBytes(alloy::hex!(
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
)))
impl Default for Calldata {
fn default() -> Self {
Self::Compound(Default::default())
}
}
/// This function takes in the string calldata argument provided in the JSON input and resolves it
/// into a [`U256`] which is later used to construct the calldata.
///
/// # Note
///
/// This piece of code is taken from the matter-labs-tester repository which is licensed under MIT
/// or Apache. The original source code can be found here:
/// https://github.com/matter-labs/era-compiler-tester/blob/0ed598a27f6eceee7008deab3ff2311075a2ec69/compiler_tester/src/test/case/input/value.rs#L43-L146
fn resolve_argument(
value: &str,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<U256> {
if let Some(instance) = value.strip_suffix(".address") {
Ok(U256::from_be_slice(
deployed_contracts
.get(&ContractInstance::new_from(instance))
.map(|(a, _)| *a)
.ok_or_else(|| anyhow::anyhow!("Instance `{}` not found", instance))?
.as_ref(),
))
} else if let Some(value) = value.strip_prefix('-') {
let value = U256::from_str_radix(value, 10)
.map_err(|error| anyhow::anyhow!("Invalid decimal literal after `-`: {}", error))?;
if value > U256::ONE << 255u8 {
anyhow::bail!("Decimal literal after `-` is too big");
impl Calldata {
pub fn new_single(item: impl Into<Bytes>) -> Self {
Self::Single(item.into())
}
pub fn new_compound(items: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
Self::Compound(
items
.into_iter()
.map(|item| item.as_ref().to_owned())
.map(CalldataItem::new)
.collect(),
)
}
pub fn find_all_contract_instances(&self, vec: &mut Vec<ContractInstance>) {
if let Calldata::Compound(compound) = self {
for item in compound {
if let Some(instance) =
item.strip_suffix(CalldataToken::<()>::ADDRESS_VARIABLE_SUFFIX)
{
vec.push(ContractInstance::new(instance))
}
}
}
let value = value
.checked_sub(U256::ONE)
.ok_or_else(|| anyhow::anyhow!("`-0` is invalid literal"))?;
Ok(U256::MAX.checked_sub(value).expect("Always valid"))
} 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 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();
}
let current_block_number = chain_state_provider.last_block_number()?;
let desired_block_number = current_block_number - offset;
pub fn calldata(
&self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<Vec<u8>> {
let mut buffer = Vec::<u8>::with_capacity(self.size_requirement());
self.calldata_into_slice(&mut buffer, deployed_contracts, chain_state_provider)?;
Ok(buffer)
}
let block_hash = chain_state_provider.block_hash(desired_block_number.into())?;
pub fn calldata_into_slice(
&self,
buffer: &mut Vec<u8>,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<()> {
match self {
Calldata::Single(bytes) => {
buffer.extend_from_slice(bytes);
}
Calldata::Compound(items) => {
for (arg_idx, arg) in items.iter().enumerate() {
match arg.resolve(deployed_contracts, chain_state_provider) {
Ok(resolved) => {
buffer.extend(resolved.to_be_bytes::<32>());
}
Err(error) => {
tracing::error!(?arg, arg_idx, ?error, "Failed to resolve argument");
return Err(error);
}
};
}
}
};
Ok(())
}
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))?)
pub fn size_requirement(&self) -> usize {
match self {
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<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<bool> {
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.as_ref() == "*" {
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 = this.resolve(deployed_contracts, chain_state_provider)?;
let other = U256::from_be_slice(&other);
if this != other {
return Ok(false);
}
}
Ok(true)
}
}
}
}
impl CalldataItem {
fn resolve(
&self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<U256> {
let mut stack = Vec::<CalldataToken<U256>>::new();
for token in self
.calldata_tokens()
.map(|token| token.resolve(deployed_contracts, chain_state_provider))
{
let token = token?;
let new_token = match token {
CalldataToken::Item(_) => token,
CalldataToken::Operation(operation) => {
let right_operand = stack
.pop()
.and_then(CalldataToken::into_item)
.context("Invalid calldata arithmetic operation")?;
let left_operand = stack
.pop()
.and_then(CalldataToken::into_item)
.context("Invalid calldata arithmetic operation")?;
let result = match operation {
Operation::Addition => left_operand.checked_add(right_operand),
Operation::Subtraction => left_operand.checked_sub(right_operand),
Operation::Multiplication => left_operand.checked_mul(right_operand),
Operation::Division => left_operand.checked_div(right_operand),
Operation::BitwiseAnd => Some(left_operand & right_operand),
Operation::BitwiseOr => Some(left_operand | right_operand),
Operation::BitwiseXor => Some(left_operand ^ right_operand),
}
.context("Invalid calldata arithmetic operation")?;
CalldataToken::Item(result)
}
};
stack.push(new_token)
}
match stack.as_slice() {
// Empty stack means that we got an empty compound calldata which we resolve to zero.
[] => Ok(U256::ZERO),
[CalldataToken::Item(item)] => Ok(*item),
_ => Err(anyhow::anyhow!("Invalid calldata arithmetic operation")),
}
}
fn calldata_tokens<'a>(&'a self) -> impl Iterator<Item = CalldataToken<&'a str>> + 'a {
self.0.split(' ').map(|item| match item {
"+" => CalldataToken::Operation(Operation::Addition),
"-" => CalldataToken::Operation(Operation::Subtraction),
"/" => CalldataToken::Operation(Operation::Division),
"*" => CalldataToken::Operation(Operation::Multiplication),
"&" => CalldataToken::Operation(Operation::BitwiseAnd),
"|" => CalldataToken::Operation(Operation::BitwiseOr),
"^" => CalldataToken::Operation(Operation::BitwiseXor),
_ => CalldataToken::Item(item),
})
}
}
impl<T> CalldataToken<T> {
const ADDRESS_VARIABLE_SUFFIX: &str = ".address";
const NEGATIVE_VALUE_PREFIX: char = '-';
const HEX_LITERAL_PREFIX: &str = "0x";
const CHAIN_VARIABLE: &str = "$CHAIN_ID";
const GAS_LIMIT_VARIABLE: &str = "$GAS_LIMIT";
const COINBASE_VARIABLE: &str = "$COINBASE";
const DIFFICULTY_VARIABLE: &str = "$DIFFICULTY";
const BLOCK_HASH_VARIABLE_PREFIX: &str = "$BLOCK_HASH";
const BLOCK_NUMBER_VARIABLE: &str = "$BLOCK_NUMBER";
const BLOCK_TIMESTAMP_VARIABLE: &str = "$BLOCK_TIMESTAMP";
fn into_item(self) -> Option<T> {
match self {
CalldataToken::Item(item) => Some(item),
CalldataToken::Operation(_) => None,
}
}
}
impl<T: AsRef<str>> CalldataToken<T> {
/// This function takes in the string calldata argument provided in the JSON input and resolves
/// it into a [`U256`] which is later used to construct the calldata.
///
/// # Note
///
/// This piece of code is taken from the matter-labs-tester repository which is licensed under
/// MIT or Apache. The original source code can be found here:
/// https://github.com/matter-labs/era-compiler-tester/blob/0ed598a27f6eceee7008deab3ff2311075a2ec69/compiler_tester/src/test/case/input/value.rs#L43-L146
fn resolve(
self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<CalldataToken<U256>> {
match self {
Self::Item(item) => {
let item = item.as_ref();
let value = if let Some(instance) = item.strip_suffix(Self::ADDRESS_VARIABLE_SUFFIX)
{
Ok(U256::from_be_slice(
deployed_contracts
.get(&ContractInstance::new(instance))
.map(|(a, _)| *a)
.ok_or_else(|| anyhow::anyhow!("Instance `{}` not found", instance))?
.as_ref(),
))
} else if let Some(value) = item.strip_prefix(Self::NEGATIVE_VALUE_PREFIX) {
let value = U256::from_str_radix(value, 10).map_err(|error| {
anyhow::anyhow!("Invalid decimal literal after `-`: {}", error)
})?;
if value > U256::ONE << 255u8 {
anyhow::bail!("Decimal literal after `-` is too big");
}
let value = value
.checked_sub(U256::ONE)
.ok_or_else(|| anyhow::anyhow!("`-0` is invalid literal"))?;
Ok(U256::MAX.checked_sub(value).expect("Always valid"))
} else if let Some(value) = item.strip_prefix(Self::HEX_LITERAL_PREFIX) {
Ok(U256::from_str_radix(value, 16).map_err(|error| {
anyhow::anyhow!("Invalid hexadecimal literal: {}", error)
})?)
} else if item == Self::CHAIN_VARIABLE {
let chain_id = chain_state_provider.chain_id()?;
Ok(U256::from(chain_id))
} else if item == Self::GAS_LIMIT_VARIABLE {
let gas_limit =
chain_state_provider.block_gas_limit(BlockNumberOrTag::Latest)?;
Ok(U256::from(gas_limit))
} else if item == Self::COINBASE_VARIABLE {
let coinbase = chain_state_provider.block_coinbase(BlockNumberOrTag::Latest)?;
Ok(U256::from_be_slice(coinbase.as_ref()))
} else if item == Self::DIFFICULTY_VARIABLE {
let block_difficulty =
chain_state_provider.block_difficulty(BlockNumberOrTag::Latest)?;
Ok(block_difficulty)
} else if item.starts_with(Self::BLOCK_HASH_VARIABLE_PREFIX) {
let offset: u64 = item
.split(':')
.next_back()
.and_then(|value| value.parse().ok())
.unwrap_or_default();
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 item == Self::BLOCK_NUMBER_VARIABLE {
let current_block_number = chain_state_provider.last_block_number()?;
Ok(U256::from(current_block_number))
} else if item == Self::BLOCK_TIMESTAMP_VARIABLE {
let timestamp =
chain_state_provider.block_timestamp(BlockNumberOrTag::Latest)?;
Ok(U256::from(timestamp))
} else {
Ok(U256::from_str_radix(item, 10)
.map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?)
};
value.map(CalldataToken::Item)
}
Self::Operation(operation) => Ok(CalldataToken::Operation(operation)),
}
}
}
impl Serialize for EtherValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
format!("{} wei", self.0).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for EtherValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let string = String::deserialize(deserializer)?;
let mut splitted = string.split(' ');
let (Some(value), Some(unit)) = (splitted.next(), splitted.next()) else {
return Err(serde::de::Error::custom("Failed to parse the value"));
};
let parsed = parse_units(value, unit.replace("eth", "ether"))
.map_err(|_| serde::de::Error::custom("Failed to parse units"))?
.into();
Ok(Self(parsed))
}
}
@@ -495,15 +687,15 @@ mod tests {
.0;
let input = Input {
instance: ContractInstance::new_from("Contract"),
instance: ContractInstance::new("Contract"),
method: Method::FunctionName("store".to_owned()),
calldata: Calldata::Compound(vec!["42".into()]),
calldata: Calldata::new_compound(["42"]),
..Default::default()
};
let mut contracts = HashMap::new();
contracts.insert(
ContractInstance::new_from("Contract"),
ContractInstance::new("Contract"),
(Address::ZERO, parsed_abi),
);
@@ -539,15 +731,13 @@ mod tests {
let input: Input = Input {
instance: "Contract".to_owned().into(),
method: Method::FunctionName("send(address)".to_owned()),
calldata: Calldata::Compound(vec![
"0x1000000000000000000000000000000000000001".to_string(),
]),
calldata: Calldata::new_compound(["0x1000000000000000000000000000000000000001"]),
..Default::default()
};
let mut contracts = HashMap::new();
contracts.insert(
ContractInstance::new_from("Contract"),
ContractInstance::new("Contract"),
(Address::ZERO, parsed_abi),
);
@@ -584,17 +774,15 @@ mod tests {
.0;
let input: Input = Input {
instance: ContractInstance::new_from("Contract"),
instance: ContractInstance::new("Contract"),
method: Method::FunctionName("send".to_owned()),
calldata: Calldata::Compound(vec![
"0x1000000000000000000000000000000000000001".to_string(),
]),
calldata: Calldata::new_compound(["0x1000000000000000000000000000000000000001"]),
..Default::default()
};
let mut contracts = HashMap::new();
contracts.insert(
ContractInstance::new_from("Contract"),
ContractInstance::new("Contract"),
(Address::ZERO, parsed_abi),
);
@@ -609,13 +797,21 @@ mod tests {
);
}
fn resolve_calldata_item(
input: &str,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<U256> {
CalldataItem::new(input).resolve(deployed_contracts, chain_state_provider)
}
#[test]
fn resolver_can_resolve_chain_id_variable() {
// Arrange
let input = "$CHAIN_ID";
// Act
let resolved = resolve_argument(input, &Default::default(), &MockResolver);
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
@@ -628,7 +824,7 @@ mod tests {
let input = "$GAS_LIMIT";
// Act
let resolved = resolve_argument(input, &Default::default(), &MockResolver);
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
@@ -644,7 +840,7 @@ mod tests {
let input = "$COINBASE";
// Act
let resolved = resolve_argument(input, &Default::default(), &MockResolver);
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
@@ -665,7 +861,7 @@ mod tests {
let input = "$DIFFICULTY";
// Act
let resolved = resolve_argument(input, &Default::default(), &MockResolver);
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
@@ -681,7 +877,7 @@ mod tests {
let input = "$BLOCK_HASH";
// Act
let resolved = resolve_argument(input, &Default::default(), &MockResolver);
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
@@ -697,7 +893,7 @@ mod tests {
let input = "$BLOCK_NUMBER";
// Act
let resolved = resolve_argument(input, &Default::default(), &MockResolver);
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
@@ -713,7 +909,7 @@ mod tests {
let input = "$BLOCK_TIMESTAMP";
// Act
let resolved = resolve_argument(input, &Default::default(), &MockResolver);
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
@@ -722,4 +918,145 @@ mod tests {
U256::from(MockResolver.block_timestamp(Default::default()).unwrap())
)
}
#[test]
fn simple_addition_can_be_resolved() {
// Arrange
let input = "2 4 +";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(resolved, U256::from(6));
}
#[test]
fn simple_subtraction_can_be_resolved() {
// Arrange
let input = "4 2 -";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(resolved, U256::from(2));
}
#[test]
fn simple_multiplication_can_be_resolved() {
// Arrange
let input = "4 2 *";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(resolved, U256::from(8));
}
#[test]
fn simple_division_can_be_resolved() {
// Arrange
let input = "4 2 /";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(resolved, U256::from(2));
}
#[test]
fn arithmetic_errors_are_not_panics() {
// Arrange
let input = "4 0 /";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
assert!(resolved.is_err())
}
#[test]
fn arithmetic_with_resolution_works() {
// Arrange
let input = "$BLOCK_NUMBER 10 +";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from(MockResolver.last_block_number().unwrap() + 10)
);
}
#[test]
fn incorrect_number_of_arguments_errors() {
// Arrange
let input = "$BLOCK_NUMBER 10 + +";
// Act
let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver);
// Assert
assert!(resolved.is_err())
}
#[test]
fn expected_json_can_be_deserialized1() {
// Arrange
let str = r#"
{
"return_data": [
"1"
],
"events": [
{
"topics": [],
"values": []
}
]
}
"#;
// Act
let expected = serde_json::from_str::<Expected>(str);
// Assert
expected.expect("Failed to deserialize");
}
#[test]
fn expected_json_can_be_deserialized2() {
// Arrange
let str = r#"
{
"return_data": [
"1"
],
"events": [
{
"address": "Main.address",
"topics": [],
"values": []
}
]
}
"#;
// Act
let expected = serde_json::from_str::<Expected>(str);
// Assert
expected.expect("Failed to deserialize");
}
}
+15 -15
View File
@@ -47,9 +47,9 @@ impl Deref for MetadataFile {
pub struct Metadata {
pub targets: Option<Vec<String>>,
pub cases: Vec<Case>,
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdentifier>>,
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdent>>,
// TODO: Convert into wrapper types for clarity.
pub libraries: Option<BTreeMap<String, BTreeMap<String, String>>>,
pub libraries: Option<BTreeMap<PathBuf, BTreeMap<ContractIdent, ContractInstance>>>,
pub ignore: Option<bool>,
pub modes: Option<Vec<Mode>>,
pub file_path: Option<PathBuf>,
@@ -86,7 +86,7 @@ impl Metadata {
/// Returns the contract sources with canonicalized paths for the files
pub fn contract_sources(
&self,
) -> anyhow::Result<BTreeMap<ContractInstance, ContractPathAndIdentifier>> {
) -> anyhow::Result<BTreeMap<ContractInstance, ContractPathAndIdent>> {
let directory = self.directory()?;
let mut sources = BTreeMap::new();
let Some(contracts) = &self.contracts else {
@@ -95,7 +95,7 @@ impl Metadata {
for (
alias,
ContractPathAndIdentifier {
ContractPathAndIdent {
contract_source_path,
contract_ident,
},
@@ -107,7 +107,7 @@ impl Metadata {
sources.insert(
alias,
ContractPathAndIdentifier {
ContractPathAndIdent {
contract_source_path: absolute_path,
contract_ident,
},
@@ -193,10 +193,10 @@ impl Metadata {
metadata.file_path = Some(path.to_path_buf());
metadata.contracts = Some(
[(
ContractInstance::new_from("test"),
ContractPathAndIdentifier {
ContractInstance::new("Test"),
ContractPathAndIdent {
contract_source_path: path.to_path_buf(),
contract_ident: ContractIdent::new_from("Test"),
contract_ident: ContractIdent::new("Test"),
},
)]
.into(),
@@ -245,7 +245,7 @@ define_wrapper_type!(
/// ```
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct ContractPathAndIdentifier {
pub struct ContractPathAndIdent {
/// The path of the contract source code relative to the directory containing the metadata file.
pub contract_source_path: PathBuf,
@@ -253,7 +253,7 @@ pub struct ContractPathAndIdentifier {
pub contract_ident: ContractIdent,
}
impl Display for ContractPathAndIdentifier {
impl Display for ContractPathAndIdent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
@@ -264,7 +264,7 @@ impl Display for ContractPathAndIdentifier {
}
}
impl FromStr for ContractPathAndIdentifier {
impl FromStr for ContractPathAndIdent {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
@@ -300,7 +300,7 @@ impl FromStr for ContractPathAndIdentifier {
}
}
impl TryFrom<String> for ContractPathAndIdentifier {
impl TryFrom<String> for ContractPathAndIdent {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
@@ -308,8 +308,8 @@ impl TryFrom<String> for ContractPathAndIdentifier {
}
}
impl From<ContractPathAndIdentifier> for String {
fn from(value: ContractPathAndIdentifier) -> Self {
impl From<ContractPathAndIdent> for String {
fn from(value: ContractPathAndIdent) -> Self {
value.to_string()
}
}
@@ -324,7 +324,7 @@ mod test {
let string = "ERC20/ERC20.sol:ERC20";
// Act
let identifier = ContractPathAndIdentifier::from_str(string);
let identifier = ContractPathAndIdent::from_str(string);
// Assert
let identifier = identifier.expect("Failed to parse");
+8 -1
View File
@@ -152,6 +152,10 @@ impl Instance {
.arg("--nodiscover")
.arg("--maxpeers")
.arg("0")
.arg("--txlookuplimit")
.arg("0")
.arg("--cache.blocklogs")
.arg("512")
.stderr(stderr_logs_file.try_clone()?)
.stdout(stdout_logs_file.try_clone()?)
.spawn()?
@@ -294,7 +298,10 @@ impl EthereumNode for Instance {
}
match provider.get_transaction_receipt(*transaction_hash).await {
Ok(Some(receipt)) => break Ok(receipt),
Ok(Some(receipt)) => {
tracing::info!(?total_wait_duration, "Found receipt");
break Ok(receipt);
}
Ok(None) => {}
Err(error) => {
let error_string = error.to_string();