Compare commits

...

8 Commits

Author SHA1 Message Date
Omar Abdulla 8c09428be5 Update the comment 2025-07-21 10:57:57 +03:00
Omar Abdulla ac6387b6f0 Support different callers 2025-07-21 10:34:57 +03:00
Omar Abdulla e5a3f0aee9 Fix tests 2025-07-21 07:31:10 +03:00
Omar Abdulla 3cdf57f7c3 Cached nonce allocator 2025-07-21 07:19:44 +03:00
Omar Abdulla dab8ffe520 Add support for exceptions 2025-07-18 21:00:44 +03:00
Omar Abdulla c913a8222f Merge branch 'bugfix/function-signature' into feature/handle-exceptions 2025-07-18 17:25:17 +03:00
Omar Abdulla c8cef4834f Allow for the use of function signatures 2025-07-18 16:37:15 +03:00
Omar Abdulla ca59a1f6a9 Handle calldata better 2025-07-18 15:52:40 +03:00
14 changed files with 747 additions and 313 deletions
Generated
+71 -65
View File
@@ -67,9 +67,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "alloy"
version = "1.0.20"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae58d888221eecf621595e2096836ce7cfc37be06bfa39d7f64aa6a3ea4c9e5b"
checksum = "8ad4eb51e7845257b70c51b38ef8d842d5e5e93196701fcbd757577971a043c6"
dependencies = [
"alloy-consensus",
"alloy-contract",
@@ -102,15 +102,16 @@ dependencies = [
[[package]]
name = "alloy-consensus"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad451f9a70c341d951bca4e811d74dbe1e193897acd17e9dbac1353698cc430b"
checksum = "ca3b746060277f3d7f9c36903bb39b593a741cb7afcb0044164c28f0e9b673f0"
dependencies = [
"alloy-eips",
"alloy-primitives",
"alloy-rlp",
"alloy-serde",
"alloy-trie",
"alloy-tx-macros",
"auto_impl",
"c-kzg",
"derive_more 2.0.1",
@@ -126,9 +127,9 @@ dependencies = [
[[package]]
name = "alloy-consensus-any"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142daffb15d5be1a2b20d2cd540edbcef03037b55d4ff69dc06beb4d06286dba"
checksum = "bf98679329fa708fa809ea596db6d974da892b068ad45e48ac1956f582edf946"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -140,9 +141,9 @@ dependencies = [
[[package]]
name = "alloy-contract"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebf25443920ecb9728cb087fe4dc04a0b290bd6ac85638c58fe94aba70f1a44e"
checksum = "a10e47f5305ea08c37b1772086c1573e9a0a257227143996841172d37d3831bb"
dependencies = [
"alloy-consensus",
"alloy-dyn-abi",
@@ -157,6 +158,7 @@ dependencies = [
"alloy-transport",
"futures",
"futures-util",
"serde_json",
"thiserror 2.0.12",
]
@@ -227,9 +229,9 @@ dependencies = [
[[package]]
name = "alloy-eips"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3056872f6da48046913e76edb5ddced272861f6032f09461aea1a2497be5ae5d"
checksum = "f562a81278a3ed83290e68361f2d1c75d018ae3b8589a314faf9303883e18ec9"
dependencies = [
"alloy-eip2124",
"alloy-eip2930",
@@ -247,15 +249,16 @@ dependencies = [
[[package]]
name = "alloy-genesis"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c98fb40f07997529235cc474de814cd7bd9de561e101716289095696c0e4639d"
checksum = "dc41384e9ab8c9b2fb387c52774d9d432656a28edcda1c2d4083e96051524518"
dependencies = [
"alloy-eips",
"alloy-primitives",
"alloy-serde",
"alloy-trie",
"serde",
"serde_with",
]
[[package]]
@@ -272,12 +275,13 @@ dependencies = [
[[package]]
name = "alloy-json-rpc"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc08b31ebf9273839bd9a01f9333cbb7a3abb4e820c312ade349dd18bdc79581"
checksum = "12c454fcfcd5d26ed3b8cae5933cbee9da5f0b05df19b46d4bd4446d1f082565"
dependencies = [
"alloy-primitives",
"alloy-sol-types",
"http",
"serde",
"serde_json",
"thiserror 2.0.12",
@@ -286,9 +290,9 @@ dependencies = [
[[package]]
name = "alloy-network"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed117b08f0cc190312bf0c38c34cf4f0dabfb4ea8f330071c587cd7160a88cb2"
checksum = "42d6d39eabe5c7b3d8f23ac47b0b683b99faa4359797114636c66e0743103d05"
dependencies = [
"alloy-consensus",
"alloy-consensus-any",
@@ -312,9 +316,9 @@ dependencies = [
[[package]]
name = "alloy-network-primitives"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7162ff7be8649c0c391f4e248d1273e85c62076703a1f3ec7daf76b283d886d"
checksum = "3704fa8b7ba9ba3f378d99b3d628c8bc8c2fc431b709947930f154e22a8368b6"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -352,9 +356,9 @@ dependencies = [
[[package]]
name = "alloy-provider"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d84eba1fd8b6fe8b02f2acd5dd7033d0f179e304bd722d11e817db570d1fa6c4"
checksum = "08800e8cbe70c19e2eb7cf3d7ff4b28bdd9b3933f8e1c8136c7d910617ba03bf"
dependencies = [
"alloy-chains",
"alloy-consensus",
@@ -380,6 +384,7 @@ dependencies = [
"either",
"futures",
"futures-utils-wasm",
"http",
"lru",
"parking_lot",
"pin-project",
@@ -395,9 +400,9 @@ dependencies = [
[[package]]
name = "alloy-pubsub"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8550f7306e0230fc835eb2ff4af0a96362db4b6fc3f25767d161e0ad0ac765bf"
checksum = "ae68457a2c2ead6bd7d7acb5bf5f1623324b1962d4f8e7b0250657a3c3ab0a0b"
dependencies = [
"alloy-json-rpc",
"alloy-primitives",
@@ -438,9 +443,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-client"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518a699422a3eab800f3dac2130d8f2edba8e4fff267b27a9c7dc6a2b0d313ee"
checksum = "162301b5a57d4d8f000bf30f4dcb82f9f468f3e5e846eeb8598dd39e7886932c"
dependencies = [
"alloy-json-rpc",
"alloy-primitives",
@@ -448,7 +453,6 @@ dependencies = [
"alloy-transport",
"alloy-transport-http",
"alloy-transport-ipc",
"async-stream",
"futures",
"pin-project",
"reqwest",
@@ -458,16 +462,15 @@ dependencies = [
"tokio-stream",
"tower",
"tracing",
"tracing-futures",
"url",
"wasmtimer",
]
[[package]]
name = "alloy-rpc-types"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c000cab4ec26a4b3e29d144e999e1c539c2fa0abed871bf90311eb3466187ca8"
checksum = "6cd8ca94ae7e2b32cc3895d9981f3772aab0b4756aa60e9ed0bcfee50f0e1328"
dependencies = [
"alloy-primitives",
"alloy-rpc-types-eth",
@@ -478,9 +481,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-any"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "508b2fbe66d952089aa694e53802327798806498cd29ff88c75135770ecaabfc"
checksum = "076b47e834b367d8618c52dd0a0d6a711ddf66154636df394805300af4923b8a"
dependencies = [
"alloy-consensus-any",
"alloy-rpc-types-eth",
@@ -489,9 +492,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-debug"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c832f2e851801093928dbb4b7bd83cd22270faf76b2e080646b806a285c8757"
checksum = "94a2a86ad7b7d718c15e79d0779bd255561b6b22968dc5ed2e7c0fbc43bb55fe"
dependencies = [
"alloy-primitives",
"serde",
@@ -499,9 +502,9 @@ dependencies = [
[[package]]
name = "alloy-rpc-types-eth"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcaf7dff0fdd756a714d58014f4f8354a1706ebf9fa2cf73431e0aeec3c9431e"
checksum = "2c2f847e635ec0be819d06e2ada4bcc4e4204026a83c4bfd78ae8d550e027ae7"
dependencies = [
"alloy-consensus",
"alloy-consensus-any",
@@ -514,14 +517,15 @@ dependencies = [
"itertools 0.14.0",
"serde",
"serde_json",
"serde_with",
"thiserror 2.0.12",
]
[[package]]
name = "alloy-rpc-types-trace"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e3507a04e868dd83219ad3cd6a8c58aefccb64d33f426b3934423a206343e84"
checksum = "6fc58180302a94c934d455eeedb3ecb99cdc93da1dbddcdbbdb79dd6fe618b2a"
dependencies = [
"alloy-primitives",
"alloy-rpc-types-eth",
@@ -533,9 +537,9 @@ dependencies = [
[[package]]
name = "alloy-serde"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "730e8f2edf2fc224cabd1c25d090e1655fa6137b2e409f92e5eec735903f1507"
checksum = "ae699248d02ade9db493bbdae61822277dc14ae0f82a5a4153203b60e34422a6"
dependencies = [
"alloy-primitives",
"serde",
@@ -544,9 +548,9 @@ dependencies = [
[[package]]
name = "alloy-signer"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b0d2428445ec13edc711909e023d7779618504c4800be055a5b940025dbafe3"
checksum = "3cf7d793c813515e2b627b19a15693960b3ed06670f9f66759396d06ebe5747b"
dependencies = [
"alloy-primitives",
"async-trait",
@@ -559,9 +563,9 @@ dependencies = [
[[package]]
name = "alloy-signer-local"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14fe6fedb7fe6e0dfae47fe020684f1d8e063274ef14bca387ddb7a6efa8ec1"
checksum = "51a424bc5a11df0d898ce0fd15906b88ebe2a6e4f17a514b51bc93946bb756bd"
dependencies = [
"alloy-consensus",
"alloy-network",
@@ -648,9 +652,9 @@ dependencies = [
[[package]]
name = "alloy-transport"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a712bdfeff42401a7dd9518f72f617574c36226a9b5414537fedc34350b73bf9"
checksum = "4f317d20f047b3de4d9728c556e2e9a92c9a507702d2016424cd8be13a74ca5e"
dependencies = [
"alloy-json-rpc",
"alloy-primitives",
@@ -671,9 +675,9 @@ dependencies = [
[[package]]
name = "alloy-transport-http"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ea5a76d7f2572174a382aedf36875bedf60bcc41116c9f031cf08040703a2dc"
checksum = "ff084ac7b1f318c87b579d221f11b748341d68b9ddaa4ffca5e62ed2b8cfefb4"
dependencies = [
"alloy-json-rpc",
"alloy-transport",
@@ -686,9 +690,9 @@ dependencies = [
[[package]]
name = "alloy-transport-ipc"
version = "1.0.9"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "606af17a7e064d219746f6d2625676122c79d78bf73dfe746d6db9ecd7dbcb85"
checksum = "edb099cdad8ed2e6a80811cdf9bbf715ebf4e34c981b4a6e2d1f9daacbf8b218"
dependencies = [
"alloy-json-rpc",
"alloy-pubsub",
@@ -706,9 +710,9 @@ dependencies = [
[[package]]
name = "alloy-trie"
version = "0.8.1"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "983d99aa81f586cef9dae38443245e585840fcf0fc58b09aee0b1f27aed1d500"
checksum = "bada1fc392a33665de0dc50d401a3701b62583c655e3522a323490a5da016962"
dependencies = [
"alloy-primitives",
"alloy-rlp",
@@ -720,6 +724,19 @@ dependencies = [
"tracing",
]
[[package]]
name = "alloy-tx-macros"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1154c8187a5ff985c95a8b2daa2fedcf778b17d7668e5e50e556c4ff9c881154"
dependencies = [
"alloy-primitives",
"darling",
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -3268,13 +3285,14 @@ dependencies = [
[[package]]
name = "nybbles"
version = "0.3.4"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8983bb634df7248924ee0c4c3a749609b5abcb082c28fffe3254b3eb3602b307"
checksum = "675b3a54e5b12af997abc8b6638b0aee51a28caedab70d4967e0d5db3a3f1d06"
dependencies = [
"alloy-rlp",
"const-hex",
"cfg-if",
"proptest",
"ruint",
"serde",
"smallvec",
]
@@ -5489,18 +5507,6 @@ dependencies = [
"valuable",
]
[[package]]
name = "tracing-futures"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
dependencies = [
"futures",
"futures-task",
"pin-project",
"tracing",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
+2 -1
View File
@@ -59,7 +59,7 @@ revive-common = { git = "https://github.com/paritytech/revive", rev = "3389865af
revive-differential = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
[workspace.dependencies.alloy]
version = "1.0"
version = "1.0.22"
default-features = false
features = [
"json-abi",
@@ -73,6 +73,7 @@ features = [
"network",
"serde",
"rpc-types-eth",
"genesis",
]
[profile.bench]
+9 -7
View File
@@ -123,16 +123,18 @@ impl Arguments {
panic!("should have a workdir configured")
}
/// Try to parse `self.account` into a [PrivateKeySigner],
/// panicing on error.
pub fn wallet(&self) -> EthereumWallet {
let signer = self
.account
pub fn signer(&self) -> PrivateKeySigner {
self.account
.parse::<PrivateKeySigner>()
.unwrap_or_else(|error| {
panic!("private key '{}' parsing error: {error}", self.account);
});
EthereumWallet::new(signer)
})
}
/// Try to parse `self.account` into a [PrivateKeySigner],
/// panicing on error.
pub fn wallet(&self) -> EthereumWallet {
EthereumWallet::new(self.signer())
}
}
+222 -37
View File
@@ -5,8 +5,11 @@ use std::marker::PhantomData;
use alloy::json_abi::JsonAbi;
use alloy::network::{Ethereum, TransactionBuilder};
use alloy::primitives::Bytes;
use alloy::rpc::types::TransactionReceipt;
use alloy::rpc::types::trace::geth::GethTrace;
use alloy::rpc::types::trace::geth::{
DefaultFrame, GethDebugTracingOptions, GethDefaultTracingOptions, GethTrace, PreStateConfig,
};
use alloy::{
primitives::Address,
rpc::types::{
@@ -19,7 +22,7 @@ use indexmap::IndexMap;
use revive_dt_compiler::{Compiler, SolidityCompiler};
use revive_dt_config::Arguments;
use revive_dt_format::case::CaseIdx;
use revive_dt_format::input::Method;
use revive_dt_format::input::{Calldata, Expected, ExpectedOutput, Method};
use revive_dt_format::metadata::{ContractInstance, ContractPathAndIdentifier};
use revive_dt_format::{input::Input, metadata::Metadata, mode::SolcMode};
use revive_dt_node_interaction::EthereumNode;
@@ -145,7 +148,10 @@ where
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
let deployment_receipts =
self.handle_contract_deployment(metadata, case_idx, input, node)?;
self.handle_input_execution(case_idx, input, deployment_receipts, 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_diff(case_idx, execution_receipt, node)
}
/// Handles the contract deployment for a given input performing it if it needs to be performed.
@@ -308,18 +314,15 @@ where
&mut self,
case_idx: CaseIdx,
input: &Input,
deployment_receipts: HashMap<ContractInstance, TransactionReceipt>,
mut deployment_receipts: HashMap<ContractInstance, TransactionReceipt>,
node: &T::Blockchain,
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
tracing::trace!("Calling execute_input for input: {input:?}");
let receipt = match input.method {
) -> anyhow::Result<TransactionReceipt> {
match input.method {
// This input was already executed when `handle_input` was called. We just need to
// lookup the transaction receipt in this case and continue on.
Method::Deployer => deployment_receipts
.get(&input.instance)
.context("Failed to find deployment receipt")?
.clone(),
.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)
@@ -337,35 +340,225 @@ where
tracing::trace!("Executing transaction for input: {input:?}");
match node.execute_transaction(tx) {
Ok(receipt) => receipt,
Ok(receipt) => Ok(receipt),
Err(err) => {
tracing::error!(
"Failed to execute transaction when executing the contract: {}, {:?}",
&*input.instance,
err
);
return Err(err);
Err(err)
}
}
}
}
}
fn handle_input_expectations(
&mut self,
case_idx: CaseIdx,
input: &Input,
execution_receipt: &TransactionReceipt,
node: &T::Blockchain,
) -> anyhow::Result<()> {
let span = tracing::info_span!("Handling input expectations");
let _guard = span.enter();
// Resolving the `input.expected` into a series of expectations that we can then assert on.
let expectations = match input {
// This is a bit of a special case and we have to support it separately on it's own. If
// it's a call to the deployer method, then the tests will assert that it "returns" the
// address of the contract. Deployments do not return the address of the contract but
// the runtime code of the contracts. Therefore, this assertion would always fail. So,
// we replace it with an assertion of "check if it succeeded"
Input {
expected: Some(Expected::Calldata(Calldata::Compound(compound))),
method: Method::Deployer,
..
} if compound.len() == 1
&& compound
.first()
.is_some_and(|first| first.contains(".address")) =>
{
vec![ExpectedOutput::new().with_success()]
}
Input {
expected: Some(Expected::Calldata(calldata)),
..
} => vec![ExpectedOutput::new().with_calldata(calldata.clone())],
Input {
expected: Some(Expected::Expected(expected)),
..
} => vec![expected.clone()],
Input {
expected: Some(Expected::ExpectedMany(expected)),
..
} => expected.clone(),
Input { expected: None, .. } => vec![ExpectedOutput::new().with_success()],
};
tracing::trace!(
"Transaction receipt for executed contract: {} - {:?}",
&*input.instance,
receipt,
);
// Note: we need to do assertions and checks on the output of the last call and this isn't
// available in the receipt. The only way to get this information is through tracing on the
// node.
let tracing_result = node
.trace_transaction(
execution_receipt,
GethDebugTracingOptions {
config: GethDefaultTracingOptions::default().with_enable_return_data(true),
..Default::default()
},
)?
.try_into_default_frame()
.expect("Impossible. We can't request default tracing and get some other type back");
let trace = node.trace_transaction(receipt.clone())?;
tracing::trace!(
"Trace result for contract: {} - {:?}",
&*input.instance,
trace
);
for expectation in expectations.iter() {
self.handle_input_expectation_item(
case_idx,
execution_receipt,
node,
expectation,
&tracing_result,
)?;
}
let diff = node.state_diff(receipt.clone())?;
Ok(())
}
Ok((receipt, trace, diff))
fn handle_input_expectation_item(
&mut self,
case_idx: CaseIdx,
execution_receipt: &TransactionReceipt,
node: &T::Blockchain,
expectation: &ExpectedOutput,
tracing_result: &DefaultFrame,
) -> 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?
// Handling the receipt state assertion.
let expected = !expectation.exception;
let actual = execution_receipt.status();
if actual != expected {
tracing::error!(
?execution_receipt,
expected,
actual,
"Transaction status assertion failed",
);
anyhow::bail!(
"Transaction status assertion failed - Expected {expected} but got {actual}",
);
}
// Handling the calldata assertion
if let Some(ref expected_calldata) = expectation.return_data {
let expected = expected_calldata
.calldata(self.deployed_contracts.entry(case_idx).or_default(), node)
.map(Bytes::from)?;
let actual = tracing_result.return_value.clone();
if !expected.starts_with(&actual) {
tracing::error!(?execution_receipt, %expected, %actual, "Calldata assertion failed");
anyhow::bail!("Calldata assertion failed - Expected {expected} but got {actual}",);
}
}
// Handling the events assertion
if let Some(ref expected_events) = expectation.events {
// Handling the events length assertion.
let expected = expected_events.len();
let actual = execution_receipt.logs().len();
if actual != expected {
tracing::error!(
?execution_receipt,
expected,
actual,
"Event count assertion failed",
);
anyhow::bail!(
"Event count assertion failed - Expected {expected} but got {actual}",
);
}
// Handling the events assertion.
for (expected_event, actual_event) in
expected_events.iter().zip(execution_receipt.logs())
{
// Handling the emitter assertion.
if let Some(expected_address) = expected_event.address {
let expected = expected_address;
let actual = actual_event.address();
if actual != expected {
tracing::error!(
?execution_receipt,
%expected,
%actual,
"Event emitter assertion failed",
);
anyhow::bail!(
"Event emitter assertion failed - Expected {expected} but got {actual}",
);
}
}
// Handling the topics assertion.
let expected = expected_event.topics.as_slice();
let actual = actual_event.topics();
if actual != expected {
tracing::error!(
?execution_receipt,
?expected,
?actual,
"Event topics assertion failed",
);
anyhow::bail!(
"Event topics assertion failed - Expected {expected:?} but got {actual:?}",
);
}
// Handling the values assertion.
let expected = &expected_event
.values
.calldata(self.deployed_contracts.entry(case_idx).or_default(), node)
.map(Bytes::from)?;
let actual = &actual_event.data().data;
if !expected.starts_with(actual) {
tracing::error!(
?execution_receipt,
?expected,
?actual,
"Event value assertion failed",
);
anyhow::bail!(
"Event value assertion failed - Expected {expected:?} but got {actual:?}",
);
}
}
}
Ok(())
}
fn handle_input_diff(
&mut self,
_: CaseIdx,
execution_receipt: TransactionReceipt,
node: &T::Blockchain,
) -> anyhow::Result<(TransactionReceipt, GethTrace, DiffMode)> {
let span = tracing::info_span!("Handling input diff");
let _guard = span.enter();
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
let trace = node.trace_transaction(&execution_receipt, trace_options)?;
let diff = node.state_diff(&execution_receipt)?;
Ok((execution_receipt, trace, diff))
}
}
@@ -505,7 +698,7 @@ where
// 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).
for (input_idx, input) in case.inputs.iter().enumerate() {
for (input_idx, input) in case.inputs_iterator().enumerate() {
let tracing_span = tracing::info_span!("Handling input", input_idx);
let _guard = tracing_span.enter();
@@ -513,7 +706,7 @@ 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)
{
Ok(result) => result,
Err(error) => {
@@ -541,7 +734,7 @@ where
.handle_input(
self.metadata,
case_idx,
input,
&input,
self.follower_node,
) {
Ok(result) => result,
@@ -589,14 +782,6 @@ where
tracing::trace!("Leader logs: {:?}", leader_receipt.logs());
tracing::trace!("Follower logs: {:?}", follower_receipt.logs());
}
if leader_receipt.status() != follower_receipt.status() {
tracing::debug!(
"Mismatch in status: leader = {}, follower = {}",
leader_receipt.status(),
follower_receipt.status()
);
}
}
// Note: Only consider the case as having been successful after we have processed
+31 -8
View File
@@ -1,5 +1,6 @@
use std::{collections::HashMap, sync::LazyLock};
use alloy::primitives::Address;
use clap::Parser;
use rayon::{ThreadPoolBuilder, prelude::*};
@@ -8,7 +9,7 @@ use revive_dt_core::{
Geth, Kitchensink, Platform,
driver::{Driver, State},
};
use revive_dt_format::{corpus::Corpus, metadata::MetadataFile};
use revive_dt_format::{corpus::Corpus, input::default_caller, metadata::MetadataFile};
use revive_dt_node::pool::NodePool;
use revive_dt_report::reporter::{Report, Span};
use temp_dir::TempDir;
@@ -20,12 +21,24 @@ static TEMP_DIR: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
fn main() -> anyhow::Result<()> {
let args = init_cli()?;
let corpora = collect_corpora(&args)?;
let additional_callers = corpora
.values()
.flat_map(|value| value.iter().map(|metadata| &metadata.cases))
.flat_map(|case| case.iter().map(|case| &case.inputs))
.flatten()
.map(|input| input.caller)
.filter(|caller| caller != &default_caller())
.collect::<Vec<_>>();
tracing::debug!(?additional_callers, "Discovered callers");
for (corpus, tests) in collect_corpora(&args)? {
let span = Span::new(corpus, args.clone())?;
match &args.compile_only {
Some(platform) => compile_corpus(&args, &tests, platform, span),
None => execute_corpus(&args, &tests, span)?,
None => execute_corpus(&args, &tests, &additional_callers, span)?,
}
Report::save()?;
@@ -83,15 +96,20 @@ fn collect_corpora(args: &Arguments) -> anyhow::Result<HashMap<Corpus, Vec<Metad
Ok(corpora)
}
fn run_driver<L, F>(args: &Arguments, tests: &[MetadataFile], span: Span) -> anyhow::Result<()>
fn run_driver<L, F>(
args: &Arguments,
tests: &[MetadataFile],
additional_callers: &[Address],
span: Span,
) -> anyhow::Result<()>
where
L: Platform,
F: Platform,
L::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
F::Blockchain: revive_dt_node::Node + Send + Sync + 'static,
{
let leader_nodes = NodePool::<L::Blockchain>::new(args)?;
let follower_nodes = NodePool::<F::Blockchain>::new(args)?;
let leader_nodes = NodePool::<L::Blockchain>::new(args, additional_callers)?;
let follower_nodes = NodePool::<F::Blockchain>::new(args, additional_callers)?;
tests.par_iter().for_each(
|MetadataFile {
@@ -141,13 +159,18 @@ where
Ok(())
}
fn execute_corpus(args: &Arguments, tests: &[MetadataFile], span: Span) -> anyhow::Result<()> {
fn execute_corpus(
args: &Arguments,
tests: &[MetadataFile],
additional_callers: &[Address],
span: Span,
) -> anyhow::Result<()> {
match (&args.leader, &args.follower) {
(TestingPlatform::Geth, TestingPlatform::Kitchensink) => {
run_driver::<Geth, Kitchensink>(args, tests, span)?
run_driver::<Geth, Kitchensink>(args, tests, additional_callers, span)?
}
(TestingPlatform::Geth, TestingPlatform::Geth) => {
run_driver::<Geth, Geth>(args, tests, span)?
run_driver::<Geth, Geth>(args, tests, additional_callers, span)?
}
_ => unimplemented!(),
}
+24 -1
View File
@@ -1,6 +1,10 @@
use serde::Deserialize;
use crate::{define_wrapper_type, input::Input, mode::Mode};
use crate::{
define_wrapper_type,
input::{Expected, Input},
mode::Mode,
};
#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)]
pub struct Case {
@@ -9,6 +13,7 @@ pub struct Case {
pub modes: Option<Vec<Mode>>,
pub inputs: Vec<Input>,
pub group: Option<String>,
pub expected: Option<Expected>,
}
define_wrapper_type!(
@@ -16,3 +21,21 @@ define_wrapper_type!(
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
CaseIdx(usize);
);
impl Case {
pub fn inputs_iterator(&self) -> impl Iterator<Item = Input> {
let inputs_len = self.inputs.len();
self.inputs
.clone()
.into_iter()
.enumerate()
.map(move |(idx, mut input)| {
if idx + 1 == inputs_len {
input.expected = self.expected.clone();
input
} else {
input
}
})
}
}
+149 -75
View File
@@ -7,9 +7,9 @@ use alloy::{
primitives::{Address, Bytes, U256},
rpc::types::TransactionRequest,
};
use alloy_primitives::B256;
use semver::VersionReq;
use serde::Deserialize;
use serde_json::Value;
use revive_dt_node_interaction::EthereumNode;
@@ -23,7 +23,8 @@ pub struct Input {
#[serde(default = "default_instance")]
pub instance: ContractInstance,
pub method: Method,
pub calldata: Option<Calldata>,
#[serde(default)]
pub calldata: Calldata,
pub expected: Option<Expected>,
pub value: Option<String>,
pub storage: Option<HashMap<String, Calldata>>,
@@ -39,10 +40,18 @@ pub enum Expected {
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct ExpectedOutput {
compiler_version: Option<VersionReq>,
return_data: Option<Calldata>,
events: Option<Value>,
exception: Option<bool>,
pub compiler_version: Option<VersionReq>,
pub return_data: Option<Calldata>,
pub events: Option<Vec<Event>>,
#[serde(default)]
pub exception: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Event {
pub address: Option<Address>,
pub topics: Vec<B256>,
pub values: Calldata,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
@@ -73,6 +82,33 @@ pub enum Method {
FunctionName(String),
}
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 {
@@ -83,12 +119,48 @@ impl Calldata {
}
}
}
}
impl ExpectedOutput {
pub fn find_all_contract_instances(&self, vec: &mut Vec<ContractInstance>) {
if let Some(ref cd) = self.return_data {
cd.find_all_contract_instances(vec);
pub fn calldata(
&self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl EthereumNode,
) -> 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 EthereumNode,
) -> anyhow::Result<()> {
match self {
Calldata::Single(string) => {
alloy::hex::decode_to_slice(string, buffer)?;
}
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() - 2) / 2,
Calldata::Compound(items) => items.len() * 32,
}
}
}
@@ -112,23 +184,9 @@ impl Input {
) -> anyhow::Result<Bytes> {
match self.method {
Method::Deployer | Method::Fallback => {
let calldata_args = match &self.calldata {
Some(Calldata::Compound(args)) => args,
_ => anyhow::bail!("Expected compound calldata for function call"),
};
let mut calldata = Vec::<u8>::with_capacity(calldata_args.len() * 32);
for (arg_idx, arg) in calldata_args.iter().enumerate() {
match resolve_argument(arg, deployed_contracts, chain_state_provider) {
Ok(resolved) => {
calldata.extend(resolved.to_be_bytes::<32>());
}
Err(error) => {
tracing::error!(arg, arg_idx, ?error, "Failed to resolve argument");
return Err(error);
}
};
}
let calldata = self
.calldata
.calldata(deployed_contracts, chain_state_provider)?;
Ok(calldata.into())
}
@@ -150,7 +208,7 @@ impl Input {
// 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(|function| function.name.starts_with(function_name))
.find(|function| function.signature().starts_with(function_name))
.ok_or_else(|| {
anyhow::anyhow!(
"Function with name {:?} not found in ABI for the instance {:?}",
@@ -161,11 +219,6 @@ impl Input {
tracing::trace!("Functions found for instance: {}", self.instance.as_ref());
let calldata_args = match &self.calldata {
Some(Calldata::Compound(args)) => args,
_ => anyhow::bail!("Expected compound calldata for function call"),
};
tracing::trace!(
"Starting encoding ABI's parameters for instance: {}",
self.instance.as_ref()
@@ -177,20 +230,13 @@ impl Input {
//
// 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::<u8>::with_capacity(4 + calldata_args.len() * 32);
let mut calldata = Vec::<u8>::with_capacity(4 + self.calldata.size_requirement());
calldata.extend(function.selector().0);
for (arg_idx, arg) in calldata_args.iter().enumerate() {
match resolve_argument(arg, deployed_contracts, chain_state_provider) {
Ok(resolved) => {
calldata.extend(resolved.to_be_bytes::<32>());
}
Err(error) => {
tracing::error!(arg, arg_idx, ?error, "Failed to resolve argument");
return Err(error);
}
};
}
self.calldata.calldata_into_slice(
&mut calldata,
deployed_contracts,
chain_state_provider,
)?;
Ok(calldata.into())
}
@@ -217,33 +263,17 @@ impl Input {
let mut vec = Vec::new();
vec.push(self.instance.clone());
if let Some(ref cd) = self.calldata {
cd.find_all_contract_instances(&mut vec);
}
match &self.expected {
Some(Expected::Calldata(cd)) => {
cd.find_all_contract_instances(&mut vec);
}
Some(Expected::Expected(expected)) => {
expected.find_all_contract_instances(&mut vec);
}
Some(Expected::ExpectedMany(expected)) => {
for expected in expected {
expected.find_all_contract_instances(&mut vec);
}
}
None => {}
}
self.calldata.find_all_contract_instances(&mut vec);
vec
}
}
fn default_instance() -> ContractInstance {
pub fn default_instance() -> ContractInstance {
ContractInstance::new_from("Test")
}
fn default_caller() -> Address {
pub fn default_caller() -> Address {
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap()
}
@@ -339,22 +369,19 @@ mod tests {
fn trace_transaction(
&self,
_: alloy::rpc::types::TransactionReceipt,
_: &alloy::rpc::types::TransactionReceipt,
_: alloy::rpc::types::trace::geth::GethDebugTracingOptions,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
unimplemented!()
}
fn state_diff(
&self,
_: alloy::rpc::types::TransactionReceipt,
_: &alloy::rpc::types::TransactionReceipt,
) -> anyhow::Result<alloy::rpc::types::trace::geth::DiffMode> {
unimplemented!()
}
fn fetch_add_nonce(&self, _: Address) -> anyhow::Result<u64> {
unimplemented!()
}
fn chain_id(&self) -> anyhow::Result<alloy_primitives::ChainId> {
Ok(0x123)
}
@@ -416,7 +443,7 @@ mod tests {
let input = Input {
instance: ContractInstance::new_from("Contract"),
method: Method::FunctionName("store".to_owned()),
calldata: Some(Calldata::Compound(vec!["42".into()])),
calldata: Calldata::Compound(vec!["42".into()]),
..Default::default()
};
@@ -458,9 +485,56 @@ mod tests {
let input: Input = Input {
instance: ContractInstance::new_from("Contract"),
method: Method::FunctionName("send".to_owned()),
calldata: Some(Calldata::Compound(vec![
calldata: Calldata::Compound(vec![
"0x1000000000000000000000000000000000000001".to_string(),
])),
]),
..Default::default()
};
let mut contracts = HashMap::new();
contracts.insert(
ContractInstance::new_from("Contract"),
(Address::ZERO, parsed_abi),
);
let encoded = input.encoded_input(&contracts, &DummyEthereumNode).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")
);
}
#[test]
fn test_encoded_input_address_with_signature() {
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 = parsed_abi
.function("send")
.unwrap()
.first()
.unwrap()
.selector()
.0;
let input: Input = Input {
instance: ContractInstance::new_from("Contract"),
method: Method::FunctionName("send(address)".to_owned()),
calldata: Calldata::Compound(vec![
"0x1000000000000000000000000000000000000001".to_string(),
]),
..Default::default()
};
+7 -6
View File
@@ -2,7 +2,7 @@
use alloy::eips::BlockNumberOrTag;
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
use alloy::rpc::types::trace::geth::{DiffMode, GethTrace};
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
use alloy::rpc::types::{TransactionReceipt, TransactionRequest};
use anyhow::Result;
@@ -15,13 +15,14 @@ pub trait EthereumNode {
fn execute_transaction(&self, transaction: TransactionRequest) -> Result<TransactionReceipt>;
/// Trace the transaction in the [TransactionReceipt] and return a [GethTrace].
fn trace_transaction(&self, transaction: TransactionReceipt) -> Result<GethTrace>;
fn trace_transaction(
&self,
receipt: &TransactionReceipt,
trace_options: GethDebugTracingOptions,
) -> Result<GethTrace>;
/// Returns the state diff of the transaction hash in the [TransactionReceipt].
fn state_diff(&self, transaction: TransactionReceipt) -> Result<DiffMode>;
/// Returns the next available nonce for the given [Address].
fn fetch_add_nonce(&self, address: Address) -> Result<u64>;
fn state_diff(&self, receipt: &TransactionReceipt) -> Result<DiffMode>;
/// Returns the ID of the chain that the node is on.
fn chain_id(&self) -> Result<ChainId>;
+108
View File
@@ -0,0 +1,108 @@
use ::core::pin::Pin;
use alloy::{
consensus::SignableTransaction,
network::{Network, TransactionBuilder, TxSigner},
primitives::Address,
providers::{
Provider, SendableTx,
fillers::{GasFiller, TxFiller},
},
signers::{Signature, local::PrivateKeySigner},
transports::TransportResult,
};
#[derive(Clone, Debug)]
pub struct FallbackGasFiller {
inner: GasFiller,
default_gas_limit: u64,
default_max_fee_per_gas: u128,
default_priority_fee: u128,
}
impl FallbackGasFiller {
pub fn new(
default_gas_limit: u64,
default_max_fee_per_gas: u128,
default_priority_fee: u128,
) -> Self {
Self {
inner: GasFiller,
default_gas_limit,
default_max_fee_per_gas,
default_priority_fee,
}
}
}
impl<N> TxFiller<N> for FallbackGasFiller
where
N: Network,
{
type Fillable = Option<<GasFiller as TxFiller<N>>::Fillable>;
fn status(
&self,
tx: &<N as Network>::TransactionRequest,
) -> alloy::providers::fillers::FillerControlFlow {
<GasFiller as TxFiller<N>>::status(&self.inner, tx)
}
fn fill_sync(&self, _: &mut alloy::providers::SendableTx<N>) {}
async fn prepare<P: Provider<N>>(
&self,
provider: &P,
tx: &<N as Network>::TransactionRequest,
) -> TransportResult<Self::Fillable> {
// Try to fetch GasFillers “fillable” (gas_price, base_fee, estimate_gas, …)
// If it errors (i.e. tx would revert under eth_estimateGas), swallow it.
match self.inner.prepare(provider, tx).await {
Ok(fill) => Ok(Some(fill)),
Err(_) => Ok(None),
}
}
async fn fill(
&self,
fillable: Self::Fillable,
mut tx: alloy::providers::SendableTx<N>,
) -> TransportResult<SendableTx<N>> {
if let Some(fill) = fillable {
// our inner GasFiller succeeded — use it
self.inner.fill(fill, tx).await
} else {
if let Some(builder) = tx.as_mut_builder() {
builder.set_gas_limit(self.default_gas_limit);
builder.set_max_fee_per_gas(self.default_max_fee_per_gas);
builder.set_max_priority_fee_per_gas(self.default_priority_fee);
}
Ok(tx)
}
}
}
/// This is a signer that is able to sign transactions for a specific address with another private
/// key.
pub struct AddressSigner {
pub private_key: PrivateKeySigner,
pub address: Address,
}
impl TxSigner<Signature> for AddressSigner {
fn address(&self) -> Address {
self.address
}
fn sign_transaction<'a, 'b, 'c>(
&'a self,
tx: &'b mut dyn SignableTransaction<Signature>,
) -> Pin<Box<dyn Future<Output = Result<Signature, alloy::signers::Error>> + Send + 'c>>
where
'a: 'c,
'b: 'c,
Self: 'c,
{
<PrivateKeySigner as TxSigner<Signature>>::sign_transaction(&self.private_key, tx)
}
}
+60 -47
View File
@@ -1,37 +1,38 @@
//! The go-ethereum node implementation.
use std::{
collections::HashMap,
fs::{File, OpenOptions, create_dir_all, remove_dir_all},
io::{BufRead, BufReader, Read, Write},
path::PathBuf,
process::{Child, Command, Stdio},
sync::{
Mutex,
atomic::{AtomicU32, Ordering},
},
sync::atomic::{AtomicU32, Ordering},
time::{Duration, Instant},
};
use alloy::{
eips::BlockNumberOrTag,
genesis::Genesis,
network::{Ethereum, EthereumWallet},
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, U256},
providers::{
Provider, ProviderBuilder,
ext::DebugApi,
fillers::{FillProvider, TxFiller},
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
},
rpc::types::{
TransactionReceipt, TransactionRequest,
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
},
signers::local::PrivateKeySigner,
};
use revive_dt_config::Arguments;
use revive_dt_node_interaction::{BlockingExecutor, EthereumNode};
use tracing::Level;
use crate::Node;
use crate::{
Node,
common::{AddressSigner, FallbackGasFiller},
};
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
@@ -54,7 +55,10 @@ pub struct Instance {
network_id: u64,
start_timeout: u64,
wallet: EthereumWallet,
nonces: Mutex<HashMap<Address, u64>>,
private_key: PrivateKeySigner,
nonce_manager: CachedNonceManager,
additional_callers: Vec<Address>,
/// This vector stores [`File`] objects that we use for logging which we want to flush when the
/// node object is dropped. We do not store them in a structured fashion at the moment (in
/// separate fields) as the logic that we need to apply to them is all the same regardless of
@@ -82,8 +86,24 @@ impl Instance {
create_dir_all(&self.base_directory)?;
create_dir_all(&self.logs_directory)?;
// Modifying the genesis file so that we get our private key to control the other accounts.
let mut genesis = serde_json::from_str::<Genesis>(&genesis)?;
for additional_caller in self.additional_callers.iter() {
let account = genesis.alloc.entry(*additional_caller).or_default();
account.private_key = Some(self.private_key.to_bytes());
*account = account
.clone()
.with_balance("1000000000000000000".parse().expect("Can't fail"));
}
let genesis_path = self.base_directory.join(Self::GENESIS_JSON_FILE);
File::create(&genesis_path)?.write_all(genesis.as_bytes())?;
serde_json::to_writer(File::create(&genesis_path)?, &genesis)?;
for additional_caller in self.additional_callers.iter() {
self.wallet.register_signer(AddressSigner {
private_key: self.private_key.clone(),
address: *additional_caller,
});
}
let mut child = Command::new(&self.geth)
.arg("init")
@@ -206,8 +226,19 @@ impl Instance {
> + 'static {
let connection_string = self.connection_string();
let wallet = self.wallet.clone();
// Note: We would like all providers to make use of the same nonce manager so that we have
// monotonically increasing nonces that are cached. The cached nonce manager uses Arc's in
// its implementation and therefore it means that when we clone it then it still references
// the same state.
let nonce_manager = self.nonce_manager.clone();
Box::pin(async move {
ProviderBuilder::new()
.disable_recommended_fillers()
.filler(FallbackGasFiller::new(500_000_000, 500_000_000, 1))
.filler(ChainIdFiller::default())
.filler(NonceFiller::new(nonce_manager))
.wallet(wallet)
.connect(&connection_string)
.await
@@ -224,7 +255,7 @@ impl EthereumNode for Instance {
) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> {
let provider = self.provider();
BlockingExecutor::execute(async move {
let outer_span = tracing::debug_span!("Submitting transaction", ?transaction,);
let outer_span = tracing::debug_span!("Submitting transaction", ?transaction);
let _outer_guard = outer_span.enter();
let provider = provider.await?;
@@ -305,30 +336,28 @@ impl EthereumNode for Instance {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn trace_transaction(
&self,
transaction: TransactionReceipt,
transaction: &TransactionReceipt,
trace_options: GethDebugTracingOptions,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
let tx_hash = transaction.transaction_hash;
let provider = self.provider();
BlockingExecutor::execute(async move {
Ok(provider
.await?
.debug_trace_transaction(transaction.transaction_hash, trace_options)
.debug_trace_transaction(tx_hash, trace_options)
.await?)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn state_diff(
&self,
transaction: alloy::rpc::types::TransactionReceipt,
) -> anyhow::Result<DiffMode> {
fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
match self
.trace_transaction(transaction)?
.trace_transaction(transaction, trace_options)?
.try_into_pre_state_frame()?
{
PreStateFrame::Diff(diff) => Ok(diff),
@@ -336,24 +365,6 @@ impl EthereumNode for Instance {
}
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64> {
let provider = self.provider();
let onchain_nonce = BlockingExecutor::execute::<anyhow::Result<_>>(async move {
provider
.await?
.get_transaction_count(address)
.await
.map_err(Into::into)
})??;
let mut nonces = self.nonces.lock().unwrap();
let current = nonces.entry(address).or_insert(onchain_nonce);
let value = *current;
*current += 1;
Ok(value)
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
let provider = self.provider();
@@ -437,7 +448,7 @@ impl EthereumNode for Instance {
}
impl Node for Instance {
fn new(config: &Arguments) -> Self {
fn new(config: &Arguments, additional_callers: &[Address]) -> Self {
let geth_directory = config.directory().join(Self::BASE_DIRECTORY);
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
let base_directory = geth_directory.join(id.to_string());
@@ -453,10 +464,12 @@ impl Node for Instance {
network_id: config.network_id,
start_timeout: config.geth_start_timeout,
wallet: config.wallet(),
nonces: Mutex::new(HashMap::new()),
// We know that we only need to be storing 2 files so we can specify that when creating
// the vector. It's the stdout and stderr of the geth node.
logs_file_to_flush: Vec::with_capacity(2),
nonce_manager: Default::default(),
additional_callers: additional_callers.to_vec(),
private_key: config.signer(),
}
}
@@ -533,7 +546,7 @@ mod tests {
fn new_node() -> (Instance, TempDir) {
let (args, temp_dir) = test_config();
let mut node = Instance::new(&args);
let mut node = Instance::new(&args, &[]);
node.init(GENESIS_JSON.to_owned())
.expect("Failed to initialize the node")
.spawn_process()
@@ -543,21 +556,21 @@ mod tests {
#[test]
fn init_works() {
Instance::new(&test_config().0)
Instance::new(&test_config().0, &[])
.init(GENESIS_JSON.to_string())
.unwrap();
}
#[test]
fn spawn_works() {
Instance::new(&test_config().0)
Instance::new(&test_config().0, &[])
.spawn(GENESIS_JSON.to_string())
.unwrap();
}
#[test]
fn version_works() {
let version = Instance::new(&test_config().0).version().unwrap();
let version = Instance::new(&test_config().0, &[]).version().unwrap();
assert!(
version.starts_with("geth version"),
"expected version string, got: '{version}'"
+45 -45
View File
@@ -1,13 +1,9 @@
use std::{
collections::HashMap,
fs::{File, OpenOptions, create_dir_all, remove_dir_all},
io::{BufRead, Write},
path::{Path, PathBuf},
process::{Child, Command, Stdio},
sync::{
Mutex,
atomic::{AtomicU32, Ordering},
},
sync::atomic::{AtomicU32, Ordering},
time::Duration,
};
@@ -23,7 +19,7 @@ use alloy::{
providers::{
Provider, ProviderBuilder,
ext::DebugApi,
fillers::{FillProvider, TxFiller},
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
},
rpc::types::{
TransactionReceipt,
@@ -40,7 +36,7 @@ use tracing::Level;
use revive_dt_config::Arguments;
use revive_dt_node_interaction::{BlockingExecutor, EthereumNode};
use crate::Node;
use crate::{Node, common::FallbackGasFiller};
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
@@ -55,7 +51,7 @@ pub struct KitchensinkNode {
logs_directory: PathBuf,
process_substrate: Option<Child>,
process_proxy: Option<Child>,
nonces: Mutex<HashMap<Address, u64>>,
nonce_manager: CachedNonceManager,
/// This vector stores [`File`] objects that we use for logging which we want to flush when the
/// node object is dropped. We do not store them in a structured fashion at the moment (in
/// separate fields) as the logic that we need to apply to them is all the same regardless of
@@ -350,9 +346,24 @@ impl KitchensinkNode {
> + 'static {
let connection_string = self.connection_string();
let wallet = self.wallet.clone();
// Note: We would like all providers to make use of the same nonce manager so that we have
// monotonically increasing nonces that are cached. The cached nonce manager uses Arc's in
// its implementation and therefore it means that when we clone it then it still references
// the same state.
let nonce_manager = self.nonce_manager.clone();
Box::pin(async move {
ProviderBuilder::new()
.disable_recommended_fillers()
.network::<KitchenSinkNetwork>()
.filler(FallbackGasFiller::new(
30_000_000,
200_000_000_000,
3_000_000_000,
))
.filler(ChainIdFiller::default())
.filler(NonceFiller::new(nonce_manager))
.wallet(wallet)
.connect(&connection_string)
.await
@@ -384,27 +395,28 @@ impl EthereumNode for KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn trace_transaction(
&self,
transaction: TransactionReceipt,
transaction: &TransactionReceipt,
trace_options: GethDebugTracingOptions,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
let tx_hash = transaction.transaction_hash;
let provider = self.provider();
BlockingExecutor::execute(async move {
Ok(provider
.await?
.debug_trace_transaction(transaction.transaction_hash, trace_options)
.debug_trace_transaction(tx_hash, trace_options)
.await?)
})?
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn state_diff(&self, transaction: TransactionReceipt) -> anyhow::Result<DiffMode> {
fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
match self
.trace_transaction(transaction)?
.trace_transaction(transaction, trace_options)?
.try_into_pre_state_frame()?
{
PreStateFrame::Diff(diff) => Ok(diff),
@@ -412,24 +424,6 @@ impl EthereumNode for KitchensinkNode {
}
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64> {
let provider = self.provider();
let onchain_nonce = BlockingExecutor::execute::<anyhow::Result<_>>(async move {
provider
.await?
.get_transaction_count(address)
.await
.map_err(Into::into)
})??;
let mut nonces = self.nonces.lock().unwrap();
let current = nonces.entry(address).or_insert(onchain_nonce);
let value = *current;
*current += 1;
Ok(value)
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
let provider = self.provider();
@@ -513,7 +507,7 @@ impl EthereumNode for KitchensinkNode {
}
impl Node for KitchensinkNode {
fn new(config: &Arguments) -> Self {
fn new(config: &Arguments, _additional_callers: &[Address]) -> Self {
let kitchensink_directory = config.directory().join(Self::BASE_DIRECTORY);
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
let base_directory = kitchensink_directory.join(id.to_string());
@@ -529,7 +523,7 @@ impl Node for KitchensinkNode {
logs_directory,
process_substrate: None,
process_proxy: None,
nonces: Mutex::new(HashMap::new()),
nonce_manager: Default::default(),
// We know that we only need to be storing 4 files so we can specify that when creating
// the vector. It's the stdout and stderr of the substrate-node and the eth-rpc.
logs_file_to_flush: Vec::with_capacity(4),
@@ -820,6 +814,12 @@ impl TransactionBuilder<KitchenSinkNetwork> for <Ethereum as Network>::Transacti
> {
Ok(wallet.sign_request(self).await?)
}
fn take_nonce(&mut self) -> Option<u64> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::take_nonce(
self,
)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -1020,7 +1020,7 @@ mod tests {
use alloy::rpc::types::TransactionRequest;
use revive_dt_config::Arguments;
use std::path::PathBuf;
use std::sync::LazyLock;
use std::sync::{LazyLock, Mutex};
use temp_dir::TempDir;
use std::fs;
@@ -1060,7 +1060,7 @@ mod tests {
let _guard = NODE_START_MUTEX.lock().unwrap();
let (args, temp_dir) = test_config();
let mut node = KitchensinkNode::new(&args);
let mut node = KitchensinkNode::new(&args, &[]);
node.init(GENESIS_JSON)
.expect("Failed to initialize the node")
.spawn_process()
@@ -1115,7 +1115,7 @@ mod tests {
}
"#;
let mut dummy_node = KitchensinkNode::new(&test_config().0);
let mut dummy_node = KitchensinkNode::new(&test_config().0, &[]);
// Call `init()`
dummy_node.init(genesis_content).expect("init failed");
@@ -1159,7 +1159,7 @@ mod tests {
}
"#;
let node = KitchensinkNode::new(&test_config().0);
let node = KitchensinkNode::new(&test_config().0, &[]);
let result = node
.extract_balance_from_genesis_file(genesis_json)
@@ -1232,7 +1232,7 @@ mod tests {
fn spawn_works() {
let (config, _temp_dir) = test_config();
let mut node = KitchensinkNode::new(&config);
let mut node = KitchensinkNode::new(&config, &[]);
node.spawn(GENESIS_JSON.to_string()).unwrap();
}
@@ -1240,7 +1240,7 @@ mod tests {
fn version_works() {
let (config, _temp_dir) = test_config();
let node = KitchensinkNode::new(&config);
let node = KitchensinkNode::new(&config, &[]);
let version = node.version().unwrap();
assert!(
@@ -1253,7 +1253,7 @@ mod tests {
fn eth_rpc_version_works() {
let (config, _temp_dir) = test_config();
let node = KitchensinkNode::new(&config);
let node = KitchensinkNode::new(&config, &[]);
let version = node.eth_rpc_version().unwrap();
assert!(
+3 -1
View File
@@ -1,8 +1,10 @@
//! This crate implements the testing nodes.
use alloy::primitives::Address;
use revive_dt_config::Arguments;
use revive_dt_node_interaction::EthereumNode;
pub mod common;
pub mod geth;
pub mod kitchensink;
pub mod pool;
@@ -13,7 +15,7 @@ pub const GENESIS_JSON: &str = include_str!("../../../genesis.json");
/// An abstract interface for testing nodes.
pub trait Node: EthereumNode {
/// Create a new uninitialized instance.
fn new(config: &Arguments) -> Self;
fn new(config: &Arguments, additional_callers: &[Address]) -> Self;
/// Spawns a node configured according to the genesis json.
///
+12 -4
View File
@@ -6,6 +6,7 @@ use std::{
thread,
};
use alloy::primitives::Address;
use anyhow::Context;
use revive_dt_config::Arguments;
@@ -23,7 +24,7 @@ where
T: Node + Send + 'static,
{
/// Create a new Pool. This will start as many nodes as there are workers in `config`.
pub fn new(config: &Arguments) -> anyhow::Result<Self> {
pub fn new(config: &Arguments, additional_callers: &[Address]) -> anyhow::Result<Self> {
let nodes = config.workers;
let genesis = read_to_string(&config.genesis_file).context(format!(
"can not read genesis file: {}",
@@ -34,7 +35,10 @@ where
for _ in 0..nodes {
let config = config.clone();
let genesis = genesis.clone();
handles.push(thread::spawn(move || spawn_node::<T>(&config, genesis)));
let additional_callers = additional_callers.to_vec();
handles.push(thread::spawn(move || {
spawn_node::<T>(&config, genesis, &additional_callers)
}));
}
let mut nodes = Vec::with_capacity(nodes);
@@ -60,8 +64,12 @@ where
}
}
fn spawn_node<T: Node + Send>(args: &Arguments, genesis: String) -> anyhow::Result<T> {
let mut node = T::new(args);
fn spawn_node<T: Node + Send>(
args: &Arguments,
genesis: String,
additional_callers: &[Address],
) -> anyhow::Result<T> {
let mut node = T::new(args, additional_callers);
tracing::info!("starting node: {}", node.connection_string());
node.spawn(genesis)?;
Ok(node)
+4 -16
View File
@@ -110,37 +110,25 @@ mod tests {
#[test]
fn try_get_windows() {
let version = List::download(List::WINDOWS_URL)
.unwrap()
.latest_release
.into();
let version = List::download(List::WINDOWS_URL).unwrap().latest_release;
GHDownloader::windows(version).download().unwrap();
}
#[test]
fn try_get_macosx() {
let version = List::download(List::MACOSX_URL)
.unwrap()
.latest_release
.into();
let version = List::download(List::MACOSX_URL).unwrap().latest_release;
GHDownloader::macosx(version).download().unwrap();
}
#[test]
fn try_get_linux() {
let version = List::download(List::LINUX_URL)
.unwrap()
.latest_release
.into();
let version = List::download(List::LINUX_URL).unwrap().latest_release;
GHDownloader::linux(version).download().unwrap();
}
#[test]
fn try_get_wasm() {
let version = List::download(List::WASM_URL)
.unwrap()
.latest_release
.into();
let version = List::download(List::WASM_URL).unwrap().latest_release;
GHDownloader::wasm(version).download().unwrap();
}
}