Compare commits

...

13 Commits

Author SHA1 Message Date
activecoder10 77606bf5de Added bail to stop execution when we have an error during deployment 2025-07-09 11:03:50 +03:00
activecoder10 f1a1ae1e9f Added new changes to format 2025-07-08 18:00:48 +03:00
activecoder10 60831d980d Fixed format 2025-07-08 17:49:07 +03:00
activecoder10 08d29f41ed Fixed format issues 2025-07-08 16:06:15 +03:00
activecoder10 36acad927d Added unit tests for ABI parameter extraction logic 2025-07-08 15:59:49 +03:00
activecoder10 9d2084834c Small refactoring 2025-07-07 12:28:05 +03:00
activecoder10 c5ab27ed82 Refactored dependencies 2025-07-04 12:37:56 +03:00
activecoder10 a0279f0c0c Added logic for ABI 2025-07-04 12:09:21 +03:00
activecoder10 48ed8db4db Parsed ABI field in order to get method parameter 2025-07-03 15:12:23 +03:00
activecoder10 3a537c2812 Added extra logging for critical part of the flow. (#27)
* Fix legacy_transaction to address for execution part

* updated polkadot-sdk to latest

* Update polkadot-sdk to latest main with fixes

* Added extra logging

* Applied some clippy improvements
2025-06-27 15:24:57 +00:00
activecoder10 4ab79ed97e Fixed the contract deployment logic. Added new tracing logging for differential for leader and follower receipt structure (#26) 2025-06-20 13:02:54 +00:00
activecoder10 ee97b62e70 Added fetch_add_nonce method for NodeInteraction trait. Added extra logging. (#25)
* added logging

* added fetch_add_nonce method

* Added nonce for legacy transaction also

* Addressed PR comments
2025-06-18 19:43:16 +00:00
xermicus e9b5a06aec fix the simple test case definition (#24)
Signed-off-by: xermicus <cyrill@parity.io>
2025-06-17 10:23:09 +00:00
13 changed files with 742 additions and 78 deletions
Generated
+19 -16
View File
@@ -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",
+2
View File
@@ -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"
+1
View File
@@ -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 }
+255 -41
View File
@@ -1,11 +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::trace::geth::GethTrace;
use alloy::rpc::types::{TransactionInput, TransactionReceipt};
use alloy::{
network::TransactionBuilder,
primitives::{Address, bytes::Bytes, map::HashMap},
primitives::{Address, TxKind, map::HashMap},
rpc::types::{
TransactionRequest,
trace::geth::{AccountState, DiffMode, GethTrace},
trace::geth::{AccountState, DiffMode},
},
};
use revive_dt_compiler::{Compiler, CompilerInput, SolidityCompiler};
@@ -14,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;
@@ -26,7 +31,8 @@ pub struct State<'a, T: Platform> {
config: &'a Arguments,
span: Span,
contracts: Contracts<T>,
deployed_contracts: HashMap<String, Address>,
deployed_contracts: StdHashMap<String, Address>,
deployed_abis: StdHashMap<String, JsonAbi>,
}
impl<'a, T> State<'a, T>
@@ -39,6 +45,7 @@ where
span,
contracts: Default::default(),
deployed_contracts: Default::default(),
deployed_abis: Default::default(),
}
}
@@ -84,10 +91,24 @@ where
task.json_output = Some(output.output.clone());
task.error = output.error;
self.contracts.insert(output.input, output.output);
if let Some(last_output) = self.contracts.values().last() {
if let Some(contracts) = &last_output.contracts {
for (file, contracts_map) in contracts {
for contract_name in contracts_map.keys() {
log::debug!("Compiled contract: {contract_name} from file: {file}");
}
}
} else {
log::warn!("Compiled contracts field is None");
}
}
Report::compilation(span, T::config_id(), task);
Ok(())
}
Err(error) => {
log::error!("Failed to compile contract: {:?}", error.to_string());
task.error = Some(error.to_string());
Err(error)
}
@@ -98,34 +119,90 @@ where
&mut self,
input: &Input,
node: &T::Blockchain,
) -> anyhow::Result<(GethTrace, DiffMode)> {
let receipt = node.execute_transaction(input.legacy_transaction(
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
log::trace!("Calling execute_input for input: {input:?}");
let nonce = node.fetch_add_nonce(input.caller)?;
log::debug!(
"Nonce calculated on the execute contract, calculated nonce {}, for contract {}, having address {} on node: {}",
&nonce,
&input.instance,
&input.caller,
std::any::type_name::<T>()
);
let tx = match input.legacy_transaction(
self.config.network_id,
0,
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:?}");
let receipt = match node.execute_transaction(tx) {
Ok(receipt) => receipt,
Err(err) => {
log::error!(
"Failed to execute transaction when executing the contract: {}, {:?}",
&input.instance,
err
);
return Err(err);
}
};
log::trace!(
"Transaction receipt for executed contract: {} - {:?}",
&input.instance,
receipt,
);
log::trace!("Transaction receipt: {:?}", receipt);
let trace = node.trace_transaction(receipt.clone())?;
log::trace!("Trace result: {:?}", trace);
log::trace!(
"Trace result for contract: {} - {:?}",
&input.instance,
trace
);
let diff = node.state_diff(receipt)?;
let diff = node.state_diff(receipt.clone())?;
Ok((trace, diff))
Ok((receipt, trace, diff))
}
pub fn deploy_contracts(&mut self, input: &Input, node: &T::Blockchain) -> anyhow::Result<()> {
log::debug!(
"Deploying contracts {}, having address {} on node: {}",
&input.instance,
&input.caller,
std::any::type_name::<T>()
);
for output in self.contracts.values() {
let Some(contract_map) = &output.contracts else {
log::debug!("No contracts in output — skipping deployment for this input.");
log::debug!(
"No contracts in output — skipping deployment for this input {}",
&input.instance
);
continue;
};
for contracts in contract_map.values() {
for (contract_name, contract) in contracts {
if contract_name != &input.instance {
continue;
}
log::debug!(
"Contract name is: {:?} and the input name is: {:?}",
&contract_name,
&input.instance
);
let bytecode = contract
.evm
@@ -134,33 +211,126 @@ where
.map(|b| b.object.clone());
let Some(code) = bytecode else {
anyhow::bail!("no bytecode for contract `{}`", contract_name);
log::error!("no bytecode for contract {contract_name}");
continue;
};
let tx = TransactionRequest::default()
.with_from(input.caller)
.with_to(Address::ZERO)
.with_input(Bytes::from(code.clone()))
.with_gas_price(20_000_000_000)
.with_gas_limit(20_000_000_000)
.with_chain_id(self.config.network_id)
.with_nonce(0);
let nonce = node.fetch_add_nonce(input.caller)?;
log::debug!(
"Calculated nonce {}, for contract {}, having address {} on node: {}",
&nonce,
&input.instance,
&input.caller,
std::any::type_name::<T>()
);
let tx = TransactionRequest {
from: Some(input.caller),
to: Some(TxKind::Create),
gas_price: Some(5_000_000),
gas: Some(5_000_000),
chain_id: Some(self.config.network_id),
nonce: Some(nonce),
input: TransactionInput::new(Bytes::from(code.into_bytes())),
..Default::default()
};
let receipt = match node.execute_transaction(tx) {
Ok(receipt) => receipt,
Err(err) => {
log::error!(
"Failed to execute transaction when deploying the contract on node : {:?}, {:?}, {:?}",
std::any::type_name::<T>(),
&contract_name,
err
);
return Err(err);
}
};
log::debug!(
"Deployment tx sent for {} with nonce {} → tx hash: {:?}, on node: {:?}",
contract_name,
nonce,
receipt.transaction_hash,
std::any::type_name::<T>(),
);
log::trace!(
"Deployed transaction receipt for contract: {} - {:?}, on node: {:?}",
&contract_name,
receipt,
std::any::type_name::<T>(),
);
let receipt = node.execute_transaction(tx)?;
let Some(address) = receipt.contract_address else {
anyhow::bail!(
"contract `{}` deployment did not return an address",
contract_name
log::error!(
"contract {contract_name} deployment did not return an address"
);
continue;
};
self.deployed_contracts
.insert(contract_name.clone(), address);
log::info!("deployed contract `{}` at {:?}", contract_name, address);
log::trace!(
"deployed contract `{}` at {:?}, on node {:?}",
contract_name,
address,
std::any::type_name::<T>()
);
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::<serde_json::Value>(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::<JsonAbi>(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);
}
}
}
}
log::debug!("Available contracts: {:?}", self.deployed_contracts.keys());
Ok(())
}
}
@@ -192,28 +362,28 @@ where
}
pub fn trace_diff_mode(label: &str, diff: &DiffMode) {
log::trace!("{} - PRE STATE:", label);
log::trace!("{label} - PRE STATE:");
for (addr, state) in &diff.pre {
Self::trace_account_state(" [pre]", addr, state);
}
log::trace!("{} - POST STATE:", label);
log::trace!("{label} - POST STATE:");
for (addr, state) in &diff.post {
Self::trace_account_state(" [post]", addr, state);
}
}
fn trace_account_state(prefix: &str, addr: &Address, state: &AccountState) {
log::trace!("{} 0x{:x}", prefix, addr);
log::trace!("{prefix} 0x{addr:x}");
if let Some(balance) = &state.balance {
log::trace!("{} balance: {}", prefix, balance);
log::trace!("{prefix} balance: {balance}");
}
if let Some(nonce) = &state.nonce {
log::trace!("{} nonce: {}", prefix, nonce);
log::trace!("{prefix} nonce: {nonce}");
}
if let Some(code) = &state.code {
log::trace!("{} code: {}", prefix, code);
log::trace!("{prefix} code: {code}");
}
}
@@ -227,12 +397,42 @@ where
for case in &self.metadata.cases {
for input in &case.inputs {
leader_state.deploy_contracts(input, self.leader_node)?;
follower_state.deploy_contracts(input, self.follower_node)?;
log::debug!("Starting deploying contract {}", &input.instance);
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);
}
let (_, leader_diff) = leader_state.execute_input(input, self.leader_node)?;
let (_, follower_diff) =
follower_state.execute_input(input, self.follower_node)?;
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) = 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.");
@@ -241,6 +441,20 @@ where
Self::trace_diff_mode("Leader", &leader_diff);
Self::trace_diff_mode("Follower", &follower_diff);
}
if leader_receipt.logs() != follower_receipt.logs() {
log::debug!("Log/event mismatch between leader and follower.");
log::trace!("Leader logs: {:?}", leader_receipt.logs());
log::trace!("Follower logs: {:?}", follower_receipt.logs());
}
if leader_receipt.status() != follower_receipt.status() {
log::debug!(
"Mismatch in status: leader = {}, follower = {}",
leader_receipt.status(),
follower_receipt.status()
);
}
}
}
}
+2
View File
@@ -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 }
+334 -12
View File
@@ -1,9 +1,13 @@
use std::collections::HashMap;
use alloy::{
json_abi::Function, network::TransactionBuilder, primitives::Address,
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;
@@ -43,7 +47,15 @@ pub struct ExpectedOutput {
#[serde(untagged)]
pub enum Calldata {
Single(String),
Compound(Vec<String>),
Compound(Vec<CalldataArg>),
}
#[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.
@@ -101,25 +113,137 @@ impl Input {
.ok_or_else(|| anyhow::anyhow!("instance {instance} not deployed"))
}
pub fn encoded_input(
&self,
deployed_abis: &HashMap<String, JsonAbi>,
deployed_contracts: &HashMap<String, Address>,
) -> anyhow::Result<Bytes> {
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<u64> = 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<String, Address>,
deployed_abis: &HashMap<String, JsonAbi>,
) -> anyhow::Result<TransactionRequest> {
let to = match self.method {
Method::Deployer => Address::ZERO,
_ => self.instance_to_address(&self.instance, deployed_contracts)?,
Method::Deployer => Some(TxKind::Create),
_ => Some(TxKind::Call(
self.instance_to_address(&self.instance, deployed_contracts)?,
)),
};
Ok(TransactionRequest::default()
.with_from(self.caller)
.with_to(to)
.with_nonce(nonce)
.with_chain_id(chain_id)
.with_gas_price(20_000_000_000)
.with_gas_limit(20_000_000_000))
let input_data = self.encoded_input(deployed_abis, deployed_contracts)?;
Ok(TransactionRequest {
from: Some(self.caller),
to,
nonce: Some(nonce),
chain_id: Some(chain_id),
gas_price: Some(5_000_000),
gas: Some(5_000_000),
input: TransactionInput::new(input_data),
..Default::default()
})
}
}
@@ -130,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")
);
}
}
+10 -4
View File
@@ -132,7 +132,7 @@ impl Metadata {
}
fn try_from_solidity(path: &Path) -> Option<Self> {
let buf = read_to_string(path)
let spec = read_to_string(path)
.inspect_err(|error| {
log::error!(
"opening JSON test metadata file '{}' error: {error}",
@@ -147,18 +147,24 @@ impl Metadata {
buf
});
if buf.is_empty() {
if spec.is_empty() {
return None;
}
match serde_json::from_str::<Self>(&buf) {
match serde_json::from_str::<Self>(&spec) {
Ok(mut metadata) => {
metadata.file_path = Some(path.to_path_buf());
let name = path
.file_name()
.expect("this should be the path to a Solidity file")
.to_str()
.expect("the file name should be valid UTF-8k");
metadata.contracts = Some([(String::from("Test"), format!("{name}:Test"))].into());
Some(metadata)
}
Err(error) => {
log::error!(
"parsing Solidity test metadata file '{}' error: {error}",
"parsing Solidity test metadata file '{}' error: '{error}' from data: {spec}",
path.display()
);
None
+5
View File
@@ -1,9 +1,11 @@
//! This crate implements all node interactions.
use alloy::primitives::Address;
use alloy::rpc::types::trace::geth::{DiffMode, GethTrace};
use alloy::rpc::types::{TransactionReceipt, TransactionRequest};
use tokio_runtime::TO_TOKIO;
pub mod nonce;
mod tokio_runtime;
pub mod trace;
pub mod transaction;
@@ -21,4 +23,7 @@ pub trait EthereumNode {
/// Returns the state diff of the transaction hash in the [TransactionReceipt].
fn state_diff(&self, transaction: TransactionReceipt) -> anyhow::Result<DiffMode>;
/// Returns the next available nonce for the given [Address].
fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64>;
}
+55
View File
@@ -0,0 +1,55 @@
use std::pin::Pin;
use alloy::{
primitives::Address,
providers::{Provider, ProviderBuilder},
};
use tokio::sync::oneshot;
use crate::{TO_TOKIO, tokio_runtime::AsyncNodeInteraction};
pub type Task = Pin<Box<dyn Future<Output = anyhow::Result<u64>> + Send>>;
pub(crate) struct Nonce {
sender: oneshot::Sender<anyhow::Result<u64>>,
task: Task,
}
impl AsyncNodeInteraction for Nonce {
type Output = anyhow::Result<u64>;
fn split(
self,
) -> (
std::pin::Pin<Box<dyn Future<Output = Self::Output> + Send>>,
oneshot::Sender<Self::Output>,
) {
(self.task, self.sender)
}
}
/// This is like `trace_transaction`, just for nonces.
pub fn fetch_onchain_nonce(
connection: String,
wallet: alloy::network::EthereumWallet,
address: Address,
) -> anyhow::Result<u64> {
let sender = TO_TOKIO.lock().unwrap().nonce_sender.clone();
let (tx, rx) = oneshot::channel();
let task: Task = Box::pin(async move {
let provider = ProviderBuilder::new()
.wallet(wallet)
.connect(&connection)
.await?;
let onchain = provider.get_transaction_count(address).await?;
Ok(onchain)
});
sender
.blocking_send(Nonce { task, sender: tx })
.expect("not in async context");
rx.blocking_recv()
.unwrap_or_else(|err| anyhow::bail!("nonce fetch failed: {err}"))
}
@@ -10,6 +10,7 @@ use tokio::spawn;
use tokio::sync::{mpsc, oneshot};
use tokio::task::JoinError;
use crate::nonce::Nonce;
use crate::trace::Trace;
use crate::transaction::Transaction;
@@ -33,6 +34,7 @@ pub(crate) trait AsyncNodeInteraction: Send + 'static {
pub(crate) struct TokioRuntime {
pub(crate) transaction_sender: mpsc::Sender<Transaction>,
pub(crate) trace_sender: mpsc::Sender<Trace>,
pub(crate) nonce_sender: mpsc::Sender<Nonce>,
}
impl TokioRuntime {
@@ -40,11 +42,13 @@ impl TokioRuntime {
let rt = Runtime::new().expect("should be able to create the tokio runtime");
let (transaction_sender, transaction_receiver) = mpsc::channel::<Transaction>(1024);
let (trace_sender, trace_receiver) = mpsc::channel::<Trace>(1024);
let (nonce_sender, nonce_receiver) = mpsc::channel::<Nonce>(1024);
thread::spawn(move || {
rt.block_on(async move {
let transaction_task = spawn(interaction::<Transaction>(transaction_receiver));
let trace_task = spawn(interaction::<Trace>(trace_receiver));
let nonce_task = spawn(interaction::<Nonce>(nonce_receiver));
if let Err(error) = transaction_task.await {
log::error!("tokio transaction task failed: {error}");
@@ -52,12 +56,16 @@ impl TokioRuntime {
if let Err(error) = trace_task.await {
log::error!("tokio trace transaction task failed: {error}");
}
if let Err(error) = nonce_task.await {
log::error!("tokio nonce task failed: {error}");
}
});
});
Self {
transaction_sender,
trace_sender,
nonce_sender,
}
}
}
+25 -2
View File
@@ -1,17 +1,22 @@
//! The go-ethereum node implementation.
use std::{
collections::HashMap,
fs::{File, create_dir_all, remove_dir_all},
io::{BufRead, BufReader, Read, Write},
path::PathBuf,
process::{Child, Command, Stdio},
sync::atomic::{AtomicU32, Ordering},
sync::{
Mutex,
atomic::{AtomicU32, Ordering},
},
thread,
time::{Duration, Instant},
};
use alloy::{
network::EthereumWallet,
primitives::Address,
providers::{Provider, ProviderBuilder, ext::DebugApi},
rpc::types::{
TransactionReceipt, TransactionRequest,
@@ -20,7 +25,8 @@ use alloy::{
};
use revive_dt_config::Arguments;
use revive_dt_node_interaction::{
EthereumNode, trace::trace_transaction, transaction::execute_transaction,
EthereumNode, nonce::fetch_onchain_nonce, trace::trace_transaction,
transaction::execute_transaction,
};
use crate::Node;
@@ -45,6 +51,7 @@ pub struct Instance {
network_id: u64,
start_timeout: u64,
wallet: EthereumWallet,
nonces: Mutex<HashMap<Address, u64>>,
}
impl Instance {
@@ -152,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)
@@ -198,6 +207,19 @@ impl EthereumNode for Instance {
_ => anyhow::bail!("expected a diff mode trace"),
}
}
fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64> {
let connection_string = self.connection_string.clone();
let wallet = self.wallet.clone();
let onchain_nonce = fetch_onchain_nonce(connection_string, wallet, address)?;
let mut nonces = self.nonces.lock().unwrap();
let current = nonces.entry(address).or_insert(onchain_nonce);
let value = *current;
*current += 1;
Ok(value)
}
}
impl Node for Instance {
@@ -216,6 +238,7 @@ impl Node for Instance {
network_id: config.network_id,
start_timeout: config.geth_start_timeout,
wallet: config.wallet(),
nonces: Mutex::new(HashMap::new()),
}
}
+25 -2
View File
@@ -1,15 +1,20 @@
use std::{
collections::HashMap,
fs::create_dir_all,
io::BufRead,
path::PathBuf,
process::{Child, Command, Stdio},
sync::atomic::{AtomicU32, Ordering},
sync::{
Mutex,
atomic::{AtomicU32, Ordering},
},
time::Duration,
};
use alloy::{
hex,
network::EthereumWallet,
primitives::Address,
providers::{Provider, ProviderBuilder, ext::DebugApi},
rpc::types::{
TransactionReceipt,
@@ -22,7 +27,8 @@ use sp_runtime::AccountId32;
use revive_dt_config::Arguments;
use revive_dt_node_interaction::{
EthereumNode, trace::trace_transaction, transaction::execute_transaction,
EthereumNode, nonce::fetch_onchain_nonce, trace::trace_transaction,
transaction::execute_transaction,
};
use crate::Node;
@@ -39,6 +45,7 @@ pub struct KitchensinkNode {
base_directory: PathBuf,
process_substrate: Option<Child>,
process_proxy: Option<Child>,
nonces: Mutex<HashMap<Address, u64>>,
}
impl KitchensinkNode {
@@ -245,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)
@@ -289,6 +298,19 @@ impl EthereumNode for KitchensinkNode {
_ => anyhow::bail!("expected a diff mode trace"),
}
}
fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64> {
let url = self.rpc_url.clone();
let wallet = self.wallet.clone();
let onchain_nonce = fetch_onchain_nonce(url, wallet, address)?;
let mut nonces = self.nonces.lock().unwrap();
let current = nonces.entry(address).or_insert(onchain_nonce);
let value = *current;
*current += 1;
Ok(value)
}
}
impl Node for KitchensinkNode {
@@ -306,6 +328,7 @@ impl Node for KitchensinkNode {
base_directory,
process_substrate: None,
process_proxy: None,
nonces: Mutex::new(HashMap::new()),
}
}