diff --git a/Cargo.lock b/Cargo.lock index 6ef31e22..dd3b36e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,9 +260,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "1.1.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ccaa79753d7bf15f06399ea76922afbfaf8d18bebed9e8fc452984b4a90dcc9" +checksum = "15516116086325c157c18261d768a20677f0f699348000ed391d4ad0dcb82530" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -325,9 +325,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.1.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18c35fc4b03ace65001676358ffbbaefe2a2b27ee50fe777c345082c7c888be8" +checksum = "6177ed26655d4e84e00b65cb494d4e0b8830e7cae7ef5d63087d445a2600fb55" dependencies = [ "alloy-rlp", "bytes", @@ -575,9 +575,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "1.1.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8612e0658964d616344f199ab251a49d48113992d81b92dab93ed855faa66383" +checksum = "a14f21d053aea4c6630687c2f4ad614bed4c81e14737a9b904798b24f30ea849" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -589,9 +589,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-expander" -version = "1.1.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a384edac7283bc4c010a355fb648082860c04b826bb7a814c45263c8f304c74" +checksum = "34d99282e7c9ef14eb62727981a985a01869e586d1dec729d3bb33679094c100" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", @@ -608,9 +608,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-input" -version = "1.1.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd588c2d516da7deb421b8c166dc60b7ae31bca5beea29ab6621fcfa53d6ca5" +checksum = "eda029f955b78e493360ee1d7bd11e1ab9f2a220a5715449babc79d6d0a01105" dependencies = [ "alloy-json-abi", "const-hex", @@ -626,9 +626,9 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "1.1.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86ddeb70792c7ceaad23e57d52250107ebbb86733e52f4a25d8dc1abc931837" +checksum = "10db1bd7baa35bc8d4a1b07efbf734e73e5ba09f2580fb8cee3483a36087ceb2" dependencies = [ "serde", "winnow", @@ -636,9 +636,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "1.1.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "584cb97bfc5746cb9dcc4def77da11694b5d6d7339be91b7480a6a68dc129387" +checksum = "58377025a47d8b8426b3e4846a251f2c1991033b27f517aade368146f6ab1dfe" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -4034,6 +4034,7 @@ dependencies = [ "revive-dt-node-interaction", "revive-dt-report", "revive-solc-json-interface", + "serde_json", "temp-dir", ] @@ -4042,6 +4043,8 @@ name = "revive-dt-format" version = "0.1.0" dependencies = [ "alloy", + "alloy-primitives", + "alloy-sol-types", "anyhow", "log", "semver 1.0.26", @@ -5143,9 +5146,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.1.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d879005cc1b5ba4e18665be9e9501d9da3a9b95f625497c4cb7ee082b532e" +checksum = "b9ac494e7266fcdd2ad80bf4375d55d27a117ea5c866c26d0e97fe5b3caeeb75" dependencies = [ "paste", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 597ef15a..9e7f8736 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ revive-dt-node-pool = { version = "0.1.0", path = "crates/node-pool" } revive-dt-report = { version = "0.1.0", path = "crates/report" } revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" } +alloy-primitives = "1.2.1" +alloy-sol-types = "1.2.1" anyhow = "1.0" clap = { version = "4", features = ["derive"] } env_logger = "0.11.8" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 7d2f9467..9cdac1c6 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -27,4 +27,5 @@ log = { workspace = true } env_logger = { workspace = true } rayon = { workspace = true } revive-solc-json-interface = { workspace = true } +serde_json = { workspace = true } temp-dir = { workspace = true } diff --git a/crates/core/src/driver/mod.rs b/crates/core/src/driver/mod.rs index 3e5ce55f..12b0a824 100644 --- a/crates/core/src/driver/mod.rs +++ b/crates/core/src/driver/mod.rs @@ -1,12 +1,14 @@ //! The test driver handles the compilation and execution of the test cases. +use alloy::json_abi::JsonAbi; use alloy::primitives::Bytes; -use alloy::rpc::types::TransactionInput; +use alloy::rpc::types::trace::geth::GethTrace; +use alloy::rpc::types::{TransactionInput, TransactionReceipt}; use alloy::{ primitives::{Address, TxKind, map::HashMap}, rpc::types::{ - TransactionReceipt, TransactionRequest, - trace::geth::{AccountState, DiffMode, GethTrace}, + TransactionRequest, + trace::geth::{AccountState, DiffMode}, }, }; use revive_dt_compiler::{Compiler, CompilerInput, SolidityCompiler}; @@ -15,6 +17,8 @@ use revive_dt_format::{input::Input, metadata::Metadata, mode::SolcMode}; use revive_dt_node_interaction::EthereumNode; use revive_dt_report::reporter::{CompilationTask, Report, Span}; use revive_solc_json_interface::SolcStandardJsonOutput; +use serde_json::Value; +use std::collections::HashMap as StdHashMap; use crate::Platform; @@ -27,7 +31,8 @@ pub struct State<'a, T: Platform> { config: &'a Arguments, span: Span, contracts: Contracts, - deployed_contracts: HashMap, + deployed_contracts: StdHashMap, + deployed_abis: StdHashMap, } impl<'a, T> State<'a, T> @@ -40,6 +45,7 @@ where span, contracts: Default::default(), deployed_contracts: Default::default(), + deployed_abis: Default::default(), } } @@ -126,15 +132,21 @@ where std::any::type_name::() ); - let tx = - match input.legacy_transaction(self.config.network_id, nonce, &self.deployed_contracts) - { - Ok(tx) => tx, - Err(err) => { - log::error!("Failed to construct legacy transaction: {err:?}"); - return Err(err); - } - }; + let tx = match input.legacy_transaction( + self.config.network_id, + nonce, + &self.deployed_contracts, + &self.deployed_abis, + ) { + Ok(tx) => { + log::debug!("Legacy transaction data: {tx:#?}"); + tx + } + Err(err) => { + log::error!("Failed to construct legacy transaction: {err:?}"); + return Err(err); + } + }; log::trace!("Executing transaction for input: {input:?}"); @@ -191,9 +203,6 @@ where &contract_name, &input.instance ); - if contract_name != &input.instance { - continue; - } let bytecode = contract .evm @@ -270,6 +279,52 @@ where address, std::any::type_name::() ); + + if let Some(Value::String(metadata_json_str)) = &contract.metadata { + log::trace!( + "metadata found for contract {contract_name}, {metadata_json_str}" + ); + + match serde_json::from_str::(metadata_json_str) { + Ok(metadata_json) => { + if let Some(abi_value) = + metadata_json.get("output").and_then(|o| o.get("abi")) + { + match serde_json::from_value::(abi_value.clone()) { + Ok(parsed_abi) => { + log::trace!( + "ABI found in metadata for contract {}", + &contract_name + ); + self.deployed_abis + .insert(contract_name.clone(), parsed_abi); + } + Err(err) => { + anyhow::bail!( + "Failed to parse ABI from metadata for contract {}: {}", + contract_name, + err + ); + } + } + } else { + anyhow::bail!( + "No ABI found in metadata for contract {}", + contract_name + ); + } + } + Err(err) => { + anyhow::bail!( + "Failed to parse metadata JSON string for contract {}: {}", + contract_name, + err + ); + } + } + } else { + anyhow::bail!("No metadata found for contract {}", contract_name); + } } } } @@ -343,14 +398,41 @@ where for case in &self.metadata.cases { for input in &case.inputs { log::debug!("Starting deploying contract {}", &input.instance); - leader_state.deploy_contracts(input, self.leader_node)?; - follower_state.deploy_contracts(input, self.follower_node)?; + if let Err(err) = leader_state.deploy_contracts(input, self.leader_node) { + log::error!("Leader deployment failed for {}: {err}", input.instance); + continue; + } else { + log::debug!("Leader deployment succeeded for {}", &input.instance); + } + + if let Err(err) = follower_state.deploy_contracts(input, self.follower_node) { + log::error!("Follower deployment failed for {}: {err}", input.instance); + continue; + } else { + log::debug!("Follower deployment succeeded for {}", &input.instance); + } log::debug!("Starting executing contract {}", &input.instance); - let (leader_receipt, _, leader_diff) = - leader_state.execute_input(input, self.leader_node)?; - let (follower_receipt, _, follower_diff) = - follower_state.execute_input(input, self.follower_node)?; + + let (leader_receipt, _, leader_diff) = match leader_state + .execute_input(input, self.leader_node) + { + Ok(result) => result, + Err(err) => { + log::error!("Leader execution failed for {}: {err}", input.instance); + continue; + } + }; + + let (follower_receipt, _, follower_diff) = match follower_state + .execute_input(input, self.follower_node) + { + Ok(result) => result, + Err(err) => { + log::error!("Follower execution failed for {}: {err}", input.instance); + continue; + } + }; if leader_diff == follower_diff { log::debug!("State diffs match between leader and follower."); diff --git a/crates/format/Cargo.toml b/crates/format/Cargo.toml index d8fc3452..e0db5ccf 100644 --- a/crates/format/Cargo.toml +++ b/crates/format/Cargo.toml @@ -10,6 +10,8 @@ rust-version.workspace = true [dependencies] alloy = { workspace = true } +alloy-primitives = { workspace = true } +alloy-sol-types = { workspace = true } anyhow = { workspace = true } log = { workspace = true } semver = { workspace = true } diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index a9d35624..bc7b6e52 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -1,10 +1,13 @@ use std::collections::HashMap; use alloy::{ - json_abi::Function, - primitives::{Address, TxKind}, - rpc::types::TransactionRequest, + hex, + json_abi::{Function, JsonAbi}, + primitives::{Address, Bytes, TxKind}, + rpc::types::{TransactionInput, TransactionRequest}, }; +use alloy_primitives::U256; +use alloy_sol_types::SolValue; use semver::VersionReq; use serde::{Deserialize, de::Deserializer}; use serde_json::Value; @@ -44,7 +47,15 @@ pub struct ExpectedOutput { #[serde(untagged)] pub enum Calldata { Single(String), - Compound(Vec), + Compound(Vec), +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum CalldataArg { + Literal(String), + /// For example: `Contract.address` + AddressRef(String), } /// Specify how the contract is called. @@ -102,12 +113,117 @@ impl Input { .ok_or_else(|| anyhow::anyhow!("instance {instance} not deployed")) } + pub fn encoded_input( + &self, + deployed_abis: &HashMap, + deployed_contracts: &HashMap, + ) -> anyhow::Result { + let Method::Function(selector) = self.method else { + return Ok(Bytes::default()); // fallback or deployer — no input + }; + + let abi = deployed_abis + .get(&self.instance) + .ok_or_else(|| anyhow::anyhow!("ABI for instance '{}' not found", &self.instance))?; + + log::trace!("ABI found for instance: {}", &self.instance); + + // Find function by selector + let function = abi + .functions() + .find(|f| f.selector().0 == selector) + .ok_or_else(|| { + anyhow::anyhow!( + "Function with selector {:?} not found in ABI for the instance {:?}", + selector, + &self.instance + ) + })?; + + log::trace!("Functions found for instance: {}", &self.instance); + + let calldata_args = match &self.calldata { + Some(Calldata::Compound(args)) => args, + _ => anyhow::bail!("Expected compound calldata for function call"), + }; + + if calldata_args.len() != function.inputs.len() { + anyhow::bail!( + "Function expects {} args, but got {}", + function.inputs.len(), + calldata_args.len() + ); + } + + log::trace!( + "Starting encoding ABI's parameters for instance: {}", + &self.instance + ); + + let mut encoded = selector.to_vec(); + + 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() + } + }; + + encoded.extend(encoded_arg); + } + + Ok(Bytes::from(encoded)) + } + /// Parse this input into a legacy transaction. pub fn legacy_transaction( &self, chain_id: u64, nonce: u64, deployed_contracts: &HashMap, + deployed_abis: &HashMap, ) -> anyhow::Result { let to = match self.method { Method::Deployer => Some(TxKind::Create), @@ -116,6 +232,8 @@ impl Input { )), }; + let input_data = self.encoded_input(deployed_abis, deployed_contracts)?; + Ok(TransactionRequest { from: Some(self.caller), to, @@ -123,6 +241,7 @@ impl Input { chain_id: Some(chain_id), gas_price: Some(5_000_000), gas: Some(5_000_000), + input: TransactionInput::new(input_data), ..Default::default() }) } @@ -135,3 +254,201 @@ fn default_instance() -> String { fn default_caller() -> Address { "90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap() } + +#[cfg(test)] +mod tests { + + use super::*; + use alloy::json_abi::JsonAbi; + use alloy_primitives::{address, keccak256}; + use std::collections::HashMap; + + #[test] + fn test_encoded_input_uint256() { + let raw_metadata = r#" + [ + { + "inputs": [{"name": "value", "type": "uint256"}], + "name": "store", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] + "#; + + let parsed_abi: JsonAbi = serde_json::from_str(raw_metadata).unwrap(); + let selector = keccak256("store(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( + "42".to_string(), + )])), + ..Default::default() + }; + + let mut deployed_abis = HashMap::new(); + deployed_abis.insert("Contract".to_string(), parsed_abi); + let deployed_contracts = HashMap::new(); + + let encoded = input + .encoded_input(&deployed_abis, &deployed_contracts) + .unwrap(); + assert!(encoded.0.starts_with(&selector)); + + type T = (u64,); + let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap(); + 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#"[ + { + "inputs": [{"name": "recipient", "type": "address"}], + "name": "send", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ]"#; + + let parsed_abi: JsonAbi = serde_json::from_str(raw_abi).unwrap(); + let selector = keccak256("send(address)".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( + "0x1000000000000000000000000000000000000001".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 = (alloy_primitives::Address,); + let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap(); + assert_eq!( + decoded.0, + address!("0x1000000000000000000000000000000000000001") + ); + } +} diff --git a/crates/node/src/geth.rs b/crates/node/src/geth.rs index f35dea5f..0b594847 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -1,6 +1,7 @@ //! The go-ethereum node implementation. use std::{ + collections::HashMap, fs::{File, create_dir_all, remove_dir_all}, io::{BufRead, BufReader, Read, Write}, path::PathBuf, @@ -15,7 +16,7 @@ use std::{ use alloy::{ network::EthereumWallet, - primitives::{Address, map::HashMap}, + primitives::Address, providers::{Provider, ProviderBuilder, ext::DebugApi}, rpc::types::{ TransactionReceipt, TransactionRequest, @@ -158,6 +159,8 @@ impl EthereumNode for Instance { let connection_string = self.connection_string(); let wallet = self.wallet.clone(); + log::debug!("Submitting transaction: {transaction:#?}"); + execute_transaction(Box::pin(async move { Ok(ProviderBuilder::new() .wallet(wallet) diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index 0285d241..638b326c 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, fs::create_dir_all, io::BufRead, path::PathBuf, @@ -13,7 +14,7 @@ use std::{ use alloy::{ hex, network::EthereumWallet, - primitives::{Address, map::HashMap}, + primitives::Address, providers::{Provider, ProviderBuilder, ext::DebugApi}, rpc::types::{ TransactionReceipt, @@ -251,6 +252,8 @@ impl EthereumNode for KitchensinkNode { let url = self.rpc_url.clone(); let wallet = self.wallet.clone(); + log::debug!("Submitting transaction: {transaction:#?}"); + execute_transaction(Box::pin(async move { Ok(ProviderBuilder::new() .wallet(wallet)