From 20da99784ea8e2d9f4dacfd7abdf1b8d86e4aa9e Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 23:51:59 +0300 Subject: [PATCH] Add resolution logic for other matterlabs variables --- Cargo.lock | 1 + crates/core/src/driver/mod.rs | 38 ++--- crates/format/Cargo.toml | 2 + crates/format/src/input.rs | 260 ++++++++++++++++++++++++++++++---- 4 files changed, 258 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b7cfb19..a8c4f41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3984,6 +3984,7 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "anyhow", + "revive-dt-node-interaction", "semver 1.0.26", "serde", "serde_json", diff --git a/crates/core/src/driver/mod.rs b/crates/core/src/driver/mod.rs index ba7020d..731bdfd 100644 --- a/crates/core/src/driver/mod.rs +++ b/crates/core/src/driver/mod.rs @@ -1,7 +1,7 @@ //! The test driver handles the compilation and execution of the test cases. use alloy::json_abi::JsonAbi; -use alloy::network::TransactionBuilder; +use alloy::network::{Ethereum, TransactionBuilder}; use alloy::rpc::types::TransactionReceipt; use alloy::rpc::types::trace::geth::GethTrace; use alloy::{ @@ -135,17 +135,21 @@ where std::any::type_name::() ); - let tx = - match input.legacy_transaction(nonce, &self.deployed_contracts, &self.deployed_abis) { - Ok(tx) => { - tracing::debug!("Legacy transaction data: {tx:#?}"); - tx - } - Err(err) => { - tracing::error!("Failed to construct legacy transaction: {err:?}"); - return Err(err); - } - }; + let tx = match input.legacy_transaction( + nonce, + &self.deployed_contracts, + &self.deployed_abis, + node, + ) { + Ok(tx) => { + tracing::debug!("Legacy transaction data: {tx:#?}"); + tx + } + Err(err) => { + tracing::error!("Failed to construct legacy transaction: {err:?}"); + return Err(err); + } + }; tracing::trace!("Executing transaction for input: {input:?}"); @@ -231,10 +235,12 @@ where // automatically fill in all of the missing fields from the provider that we // are using. let code = alloy::hex::decode(&code)?; - let tx = TransactionRequest::default() - .nonce(nonce) - .from(input.caller) - .with_deploy_code(code); + let tx = { + let tx = TransactionRequest::default() + .nonce(nonce) + .from(input.caller); + TransactionBuilder::::with_deploy_code(tx, code) + }; let receipt = match node.execute_transaction(tx) { Ok(receipt) => receipt, diff --git a/crates/format/Cargo.toml b/crates/format/Cargo.toml index 4352683..c1b7674 100644 --- a/crates/format/Cargo.toml +++ b/crates/format/Cargo.toml @@ -9,6 +9,8 @@ repository.workspace = true rust-version.workspace = true [dependencies] +revive-dt-node-interaction = { workspace = true } + alloy = { workspace = true } alloy-primitives = { workspace = true } alloy-sol-types = { workspace = true } diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index 270d908..df868c2 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use alloy::{ + eips::BlockNumberOrTag, json_abi::JsonAbi, network::TransactionBuilder, primitives::{Address, Bytes, U256}, @@ -10,6 +11,8 @@ use semver::VersionReq; use serde::Deserialize; use serde_json::Value; +use revive_dt_node_interaction::EthereumNode; + #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] pub struct Input { #[serde(default = "default_caller")] @@ -84,6 +87,7 @@ impl Input { &self, deployed_abis: &HashMap, deployed_contracts: &HashMap, + chain_state_provider: &impl EthereumNode, ) -> anyhow::Result { let Method::FunctionName(ref function_name) = self.method else { return Ok(Bytes::default()); // fallback or deployer — no input @@ -140,7 +144,7 @@ impl Input { calldata.extend(function.selector().0); for (arg_idx, arg) in calldata_args.iter().enumerate() { - match resolve_argument(arg, deployed_contracts) { + match resolve_argument(arg, deployed_contracts, chain_state_provider) { Ok(resolved) => { calldata.extend(resolved.to_be_bytes::<32>()); } @@ -160,8 +164,10 @@ impl Input { nonce: u64, deployed_contracts: &HashMap, deployed_abis: &HashMap, + chain_state_provider: &impl EthereumNode, ) -> anyhow::Result { - let input_data = self.encoded_input(deployed_abis, deployed_contracts)?; + let input_data = + self.encoded_input(deployed_abis, deployed_contracts, chain_state_provider)?; let transaction_request = TransactionRequest::default().nonce(nonce); match self.method { Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)), @@ -191,6 +197,7 @@ fn default_caller() -> Address { fn resolve_argument( value: &str, deployed_contracts: &HashMap, + chain_state_provider: &impl EthereumNode, ) -> anyhow::Result { if let Some(instance) = value.strip_suffix(".address") { Ok(U256::from_be_slice( @@ -212,30 +219,40 @@ fn resolve_argument( } 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)); + } 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(); - 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))?) - } + 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 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))?) } } @@ -248,6 +265,69 @@ mod tests { use alloy_sol_types::SolValue; use std::collections::HashMap; + struct DummyEthereumNode; + + impl EthereumNode for DummyEthereumNode { + fn execute_transaction( + &self, + _: TransactionRequest, + ) -> anyhow::Result { + unimplemented!() + } + + fn trace_transaction( + &self, + _: alloy::rpc::types::TransactionReceipt, + ) -> anyhow::Result { + unimplemented!() + } + + fn state_diff( + &self, + _: alloy::rpc::types::TransactionReceipt, + ) -> anyhow::Result { + unimplemented!() + } + + fn fetch_add_nonce(&self, _: Address) -> anyhow::Result { + unimplemented!() + } + + fn chain_id(&self) -> anyhow::Result { + Ok(0x123) + } + + fn block_gas_limit(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result { + Ok(0x1234) + } + + fn block_coinbase(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result
{ + Ok(Address::ZERO) + } + + fn block_difficulty(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result { + Ok(U256::from(0x12345u128)) + } + + fn block_hash( + &self, + _: alloy::eips::BlockNumberOrTag, + ) -> anyhow::Result { + Ok([0xEE; 32].into()) + } + + fn block_timestamp( + &self, + _: alloy::eips::BlockNumberOrTag, + ) -> anyhow::Result { + Ok(0x123456) + } + + fn last_block_number(&self) -> anyhow::Result { + Ok(0x1234567) + } + } + #[test] fn test_encoded_input_uint256() { let raw_metadata = r#" @@ -283,7 +363,7 @@ mod tests { let deployed_contracts = HashMap::new(); let encoded = input - .encoded_input(&deployed_abis, &deployed_contracts) + .encoded_input(&deployed_abis, &deployed_contracts, &DummyEthereumNode) .unwrap(); assert!(encoded.0.starts_with(&selector)); @@ -326,7 +406,9 @@ mod tests { abis.insert("Contract".to_string(), parsed_abi); let contracts = HashMap::new(); - let encoded = input.encoded_input(&abis, &contracts).unwrap(); + let encoded = input + .encoded_input(&abis, &contracts, &DummyEthereumNode) + .unwrap(); assert!(encoded.0.starts_with(&selector)); type T = (alloy_primitives::Address,); @@ -336,4 +418,128 @@ mod tests { address!("0x1000000000000000000000000000000000000001") ); } + + #[test] + fn resolver_can_resolve_chain_id_variable() { + // Arrange + let input = "$CHAIN_ID"; + + // Act + let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!(resolved, U256::from(DummyEthereumNode.chain_id().unwrap())) + } + + #[test] + fn resolver_can_resolve_gas_limit_variable() { + // Arrange + let input = "$GAS_LIMIT"; + + // Act + let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!( + resolved, + U256::from( + DummyEthereumNode + .block_gas_limit(Default::default()) + .unwrap() + ) + ) + } + + #[test] + fn resolver_can_resolve_coinbase_variable() { + // Arrange + let input = "$COINBASE"; + + // Act + let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!( + resolved, + U256::from_be_slice( + DummyEthereumNode + .block_coinbase(Default::default()) + .unwrap() + .as_ref() + ) + ) + } + + #[test] + fn resolver_can_resolve_block_difficulty_variable() { + // Arrange + let input = "$DIFFICULTY"; + + // Act + let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!( + resolved, + DummyEthereumNode + .block_difficulty(Default::default()) + .unwrap() + ) + } + + #[test] + fn resolver_can_resolve_block_hash_variable() { + // Arrange + let input = "$BLOCK_HASH"; + + // Act + let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!( + resolved, + U256::from_be_bytes(DummyEthereumNode.block_hash(Default::default()).unwrap().0) + ) + } + + #[test] + fn resolver_can_resolve_block_number_variable() { + // Arrange + let input = "$BLOCK_NUMBER"; + + // Act + let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!( + resolved, + U256::from(DummyEthereumNode.last_block_number().unwrap()) + ) + } + + #[test] + fn resolver_can_resolve_block_timestamp_variable() { + // Arrange + let input = "$BLOCK_TIMESTAMP"; + + // Act + let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!( + resolved, + U256::from( + DummyEthereumNode + .block_timestamp(Default::default()) + .unwrap() + ) + ) + } }