diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index 34a7614..270d908 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -1,16 +1,13 @@ use std::collections::HashMap; use alloy::{ - hex, - json_abi::{Function, JsonAbi}, + json_abi::JsonAbi, network::TransactionBuilder, - primitives::{Address, Bytes}, + primitives::{Address, Bytes, U256}, rpc::types::TransactionRequest, }; -use alloy_primitives::U256; -use alloy_sol_types::SolValue; use semver::VersionReq; -use serde::{Deserialize, de::Deserializer}; +use serde::Deserialize; use serde_json::Value; #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] @@ -20,7 +17,6 @@ pub struct Input { pub comment: Option, #[serde(default = "default_instance")] pub instance: String, - #[serde(deserialize_with = "deserialize_method")] pub method: Method, pub calldata: Option, pub expected: Option, @@ -48,58 +44,28 @@ pub struct ExpectedOutput { #[serde(untagged)] pub enum Calldata { Single(String), - Compound(Vec), -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum CalldataArg { - Literal(String), - /// For example: `Contract.address` - AddressRef(String), + Compound(Vec), } /// Specify how the contract is called. -#[derive(Debug, Default, Clone, Eq, PartialEq)] +#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)] pub enum Method { /// Initiate a deploy transaction, calling contracts constructor. /// /// Indicated by `#deployer`. + #[serde(rename = "#deployer")] Deployer, + /// Does not calculate and insert a function selector. /// /// Indicated by `#fallback`. #[default] + #[serde(rename = "#fallback")] Fallback, - /// Call the public function with this selector. - /// - /// Calculates the selector if neither deployer or fallback matches. - Function([u8; 4]), -} -fn deserialize_method<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - Ok(match String::deserialize(deserializer)?.as_str() { - "#deployer" => Method::Deployer, - "#fallback" => Method::Fallback, - signature => { - let signature = if signature.ends_with(')') { - signature.to_string() - } else { - format!("{signature}()") - }; - match Function::parse(&signature) { - Ok(function) => Method::Function(function.selector().0), - Err(error) => { - return Err(serde::de::Error::custom(format!( - "parsing function signature '{signature}' error: {error}" - ))); - } - } - } - }) + /// Call the public function with the given name. + #[serde(untagged)] + FunctionName(String), } impl Input { @@ -119,7 +85,7 @@ impl Input { deployed_abis: &HashMap, deployed_contracts: &HashMap, ) -> anyhow::Result { - let Method::Function(selector) = self.method else { + let Method::FunctionName(ref function_name) = self.method else { return Ok(Bytes::default()); // fallback or deployer — no input }; @@ -129,14 +95,17 @@ impl Input { tracing::trace!("ABI found for instance: {}", &self.instance); - // Find function by selector + // We follow the same logic that's implemented in the matter-labs-tester where they resolve + // the function name into a function selector and they assume that he function doesn't have + // any existing overloads. + // https://github.com/matter-labs/era-compiler-tester/blob/1dfa7d07cba0734ca97e24704f12dd57f6990c2c/compiler_tester/src/test/case/input/mod.rs#L158-L190 let function = abi .functions() - .find(|f| f.selector().0 == selector) + .find(|function| function.name.starts_with(function_name)) .ok_or_else(|| { anyhow::anyhow!( - "Function with selector {:?} not found in ABI for the instance {:?}", - selector, + "Function with name {:?} not found in ABI for the instance {:?}", + function_name, &self.instance ) })?; @@ -161,61 +130,28 @@ impl Input { &self.instance ); - let mut encoded = selector.to_vec(); + // Allocating a vector that we will be using for the calldata. The vector size will be: + // 4 bytes for the function selector. + // function.inputs.len() * 32 bytes for the arguments (each argument is a U256). + // + // We're using indices in the following code in order to avoid the need for us to allocate + // a new buffer for each one of the resolved arguments. + let mut calldata = Vec::::with_capacity(4 + calldata_args.len() * 32); + calldata.extend(function.selector().0); - for (i, param) in function.inputs.iter().enumerate() { - let arg = calldata_args.get(i).unwrap(); - let encoded_arg = match arg { - CalldataArg::Literal(value) => match param.ty.as_str() { - "uint256" | "uint" => { - let val: U256 = value.parse()?; - val.abi_encode() - } - "uint24" => { - let val: u32 = value.parse()?; - (val & 0xFFFFFF).abi_encode() - } - "bool" => { - let val: bool = value.parse()?; - val.abi_encode() - } - "address" => { - let addr: Address = value.parse()?; - addr.abi_encode() - } - "string" => value.abi_encode(), - "bytes32" => { - let val = hex::decode(value.trim_start_matches("0x"))?; - let mut fixed = [0u8; 32]; - fixed[..val.len()].copy_from_slice(&val); - fixed.abi_encode() - } - "uint256[]" | "uint[]" => { - let nums: Vec = serde_json::from_str(value)?; - nums.abi_encode() - } - "bytes" => { - let val = hex::decode(value.trim_start_matches("0x"))?; - val.abi_encode() - } - _ => anyhow::bail!("Unsupported type: {}", param.ty), - }, - CalldataArg::AddressRef(name) => { - let contract_name = name.trim_end_matches(".address"); - let addr = deployed_contracts - .get(contract_name) - .copied() - .ok_or_else(|| { - anyhow::anyhow!("Address for '{}' not found", contract_name) - })?; - addr.abi_encode() + for (arg_idx, arg) in calldata_args.iter().enumerate() { + match resolve_argument(arg, deployed_contracts) { + Ok(resolved) => { + calldata.extend(resolved.to_be_bytes::<32>()); + } + Err(error) => { + tracing::error!(arg, arg_idx, ?error, "Failed to resolve argument"); + return Err(error); } }; - - encoded.extend(encoded_arg); } - Ok(Bytes::from(encoded)) + Ok(calldata.into()) } /// Parse this input into a legacy transaction. @@ -244,12 +180,72 @@ fn default_caller() -> Address { "90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap() } +/// 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, +) -> anyhow::Result { + if let Some(instance) = value.strip_suffix(".address") { + Ok(U256::from_be_slice( + deployed_contracts + .get(instance) + .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"); + } + 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 { + // TODO: This is a set of "variables" that we need to be able to resolve to be fully in + // compliance with the matter labs tester but we currently do not resolve them. We need to + // add logic that does their resolution in the future, perhaps through some kind of system + // context API that we pass down to the resolution function that allows it to make calls to + // the node to perform these resolutions. + let is_unsupported = [ + "$CHAIN_ID", + "$GAS_LIMIT", + "$COINBASE", + "$DIFFICULTY", + "$BLOCK_HASH", + "$BLOCK_TIMESTAMP", + ] + .iter() + .any(|var| value.starts_with(var)); + + if is_unsupported { + tracing::error!(value, "Unsupported variable used"); + anyhow::bail!("Encountered {value} which is currently unsupported by the framework"); + } else { + Ok(U256::from_str_radix(value, 10) + .map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?) + } + } +} + #[cfg(test)] mod tests { use super::*; use alloy::json_abi::JsonAbi; - use alloy_primitives::{address, keccak256}; + use alloy_primitives::address; + use alloy_sol_types::SolValue; use std::collections::HashMap; #[test] @@ -267,16 +263,18 @@ mod tests { "#; let parsed_abi: JsonAbi = serde_json::from_str(raw_metadata).unwrap(); - let selector = keccak256("store(uint256)".as_bytes())[0..4] - .try_into() - .unwrap(); + let selector = parsed_abi + .function("store") + .unwrap() + .first() + .unwrap() + .selector() + .0; let input = Input { instance: "Contract".to_string(), - method: Method::Function(selector), - calldata: Some(Calldata::Compound(vec![CalldataArg::Literal( - "42".to_string(), - )])), + method: Method::FunctionName("store".to_owned()), + calldata: Some(Calldata::Compound(vec!["42".into()])), ..Default::default() }; @@ -294,112 +292,6 @@ mod tests { assert_eq!(decoded.0, 42); } - #[test] - fn test_encoded_input_bool() { - let raw_abi = r#"[ - { - "inputs": [{"name": "flag", "type": "bool"}], - "name": "toggle", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } - ]"#; - - let parsed_abi: JsonAbi = serde_json::from_str(raw_abi).unwrap(); - let selector = keccak256("toggle(bool)".as_bytes())[0..4] - .try_into() - .unwrap(); - - let input = Input { - instance: "Contract".to_string(), - method: Method::Function(selector), - calldata: Some(Calldata::Compound(vec![CalldataArg::Literal( - "true".to_string(), - )])), - ..Default::default() - }; - - let mut abis = HashMap::new(); - abis.insert("Contract".to_string(), parsed_abi); - let contracts = HashMap::new(); - - let encoded = input.encoded_input(&abis, &contracts).unwrap(); - assert!(encoded.0.starts_with(&selector)); - - type T = (bool,); - let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap(); - assert_eq!(decoded.0, true); - } - - #[test] - fn test_encoded_input_string() { - let raw_abi = r#"[ - { - "inputs": [{"name": "msg", "type": "string"}], - "name": "echo", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } - ]"#; - - let parsed_abi: JsonAbi = serde_json::from_str(raw_abi).unwrap(); - let selector = keccak256("echo(string)".as_bytes())[0..4] - .try_into() - .unwrap(); - - let input = Input { - instance: "Contract".to_string(), - method: Method::Function(selector), - calldata: Some(Calldata::Compound(vec![CalldataArg::Literal( - "hello".to_string(), - )])), - ..Default::default() - }; - - let mut abis = HashMap::new(); - abis.insert("Contract".to_string(), parsed_abi); - let contracts = HashMap::new(); - - let encoded = input.encoded_input(&abis, &contracts).unwrap(); - assert!(encoded.0.starts_with(&selector)); - } - - #[test] - fn test_encoded_input_uint256_array() { - let raw_abi = r#"[ - { - "inputs": [{"name": "arr", "type": "uint256[]"}], - "name": "sum", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } - ]"#; - - let parsed_abi: JsonAbi = serde_json::from_str(raw_abi).unwrap(); - let selector = keccak256("sum(uint256[])".as_bytes())[0..4] - .try_into() - .unwrap(); - - let input = Input { - instance: "Contract".to_string(), - method: Method::Function(selector), - calldata: Some(Calldata::Compound(vec![CalldataArg::Literal( - "[1,2,3]".to_string(), - )])), - ..Default::default() - }; - - let mut abis = HashMap::new(); - abis.insert("Contract".to_string(), parsed_abi); - let contracts = HashMap::new(); - - let encoded = input.encoded_input(&abis, &contracts).unwrap(); - assert!(encoded.0.starts_with(&selector)); - } - #[test] fn test_encoded_input_address() { let raw_abi = r#"[ @@ -413,16 +305,20 @@ mod tests { ]"#; let parsed_abi: JsonAbi = serde_json::from_str(raw_abi).unwrap(); - let selector = keccak256("send(address)".as_bytes())[0..4] - .try_into() - .unwrap(); + let selector = parsed_abi + .function("send") + .unwrap() + .first() + .unwrap() + .selector() + .0; - let input = Input { + let input: Input = Input { instance: "Contract".to_string(), - method: Method::Function(selector), - calldata: Some(Calldata::Compound(vec![CalldataArg::Literal( + method: Method::FunctionName("send".to_owned()), + calldata: Some(Calldata::Compound(vec![ "0x1000000000000000000000000000000000000001".to_string(), - )])), + ])), ..Default::default() };