Compare commits

..

20 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
Omar Abdulla adc0c44cde Merge remote-tracking branch 'origin/main' into refactor/contract-deployment-and-input-handling 2025-07-18 15:18:27 +03:00
Omar Abdulla 811e17136b Merge remote-tracking branch 'origin/main' into refactor/contract-deployment-and-input-handling 2025-07-18 15:11:40 +03:00
Omar Abdulla ba32bad6b3 Fix edge-case in deployment order 2025-07-17 22:26:49 +03:00
Omar Abdulla bb754cba4f Correct comment 2025-07-17 18:53:24 +03:00
Omar Abdulla c858bbe66d Ignore macro doc comment tests 2025-07-17 18:28:23 +03:00
Omar Abdulla 906878f06a Fix edge-case in input handling 2025-07-17 18:00:49 +03:00
Omar Abdulla 9a71369e8a Implement the new input handling logic 2025-07-17 17:46:40 +03:00
Omar Abdulla 84ab873b46 Impl new_from for wrapper types 2025-07-17 15:33:28 +03:00
Omar Abdulla 2ef6f7ba63 Make metadata structs more typed 2025-07-17 15:31:18 +03:00
Omar Abdulla 38e6140a7c Remove unneeded use of two HashMaps 2025-07-17 14:41:48 +03:00
Omar Abdulla ca6c5529e2 Move FilesWithExtensionIterator to core::common 2025-07-17 14:32:55 +03:00
Omar Abdulla 038a2db53c Add support for wrapper types 2025-07-17 14:22:18 +03:00
31 changed files with 490 additions and 594 deletions
Generated
+9 -19
View File
@@ -346,7 +346,7 @@ dependencies = [
"keccak-asm",
"paste",
"proptest",
"rand 0.9.2",
"rand 0.9.1",
"ruint",
"rustc-hash",
"serde",
@@ -3737,9 +3737,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.2"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
@@ -3948,17 +3948,6 @@ dependencies = [
"serde_stacker",
]
[[package]]
name = "revive-dt-common"
version = "0.1.0"
dependencies = [
"anyhow",
"futures",
"once_cell",
"tokio",
"tracing",
]
[[package]]
name = "revive-dt-compiler"
version = "0.1.0"
@@ -3993,7 +3982,6 @@ dependencies = [
"clap",
"indexmap 2.10.0",
"rayon",
"revive-dt-common",
"revive-dt-compiler",
"revive-dt-config",
"revive-dt-format",
@@ -4015,7 +4003,7 @@ dependencies = [
"alloy-primitives",
"alloy-sol-types",
"anyhow",
"revive-dt-common",
"revive-dt-node-interaction",
"semver 1.0.26",
"serde",
"serde_json",
@@ -4028,9 +4016,7 @@ version = "0.1.0"
dependencies = [
"alloy",
"anyhow",
"revive-dt-common",
"revive-dt-config",
"revive-dt-format",
"revive-dt-node-interaction",
"serde",
"serde_json",
@@ -4047,6 +4033,10 @@ version = "0.1.0"
dependencies = [
"alloy",
"anyhow",
"futures",
"once_cell",
"tokio",
"tracing",
]
[[package]]
@@ -4141,7 +4131,7 @@ dependencies = [
"primitive-types 0.12.2",
"proptest",
"rand 0.8.5",
"rand 0.9.2",
"rand 0.9.1",
"rlp",
"ruint-macro",
"serde",
-1
View File
@@ -11,7 +11,6 @@ repository = "https://github.com/paritytech/revive-differential-testing.git"
rust-version = "1.85.0"
[workspace.dependencies]
revive-dt-common = { version = "0.1.0", path = "crates/common" }
revive-dt-compiler = { version = "0.1.0", path = "crates/compiler" }
revive-dt-config = { version = "0.1.0", path = "crates/config" }
revive-dt-core = { version = "0.1.0", path = "crates/core" }
-16
View File
@@ -1,16 +0,0 @@
[package]
name = "revive-dt-common"
description = "A library containing common concepts that other crates in the workspace can rely on"
version.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow = { workspace = true }
futures = { workspace = true }
tracing = { workspace = true }
once_cell = { workspace = true }
tokio = { workspace = true }
-3
View File
@@ -1,3 +0,0 @@
mod blocking_executor;
pub use blocking_executor::*;
-3
View File
@@ -1,3 +0,0 @@
mod files_with_extension_iterator;
pub use files_with_extension_iterator::*;
-6
View File
@@ -1,6 +0,0 @@
//! This crate provides common concepts, functionality, types, macros, and more that other crates in
//! the workspace can benefit from.
pub mod concepts;
pub mod iterators;
pub mod macros;
-3
View File
@@ -1,3 +0,0 @@
mod define_wrapper_type;
pub use define_wrapper_type::*;
+9 -13
View File
@@ -73,12 +73,6 @@ pub struct Arguments {
)]
pub account: String,
/// This argument controls which private keys the nodes should have access to and be added to
/// its wallet signers. With a value of N, private keys (0, N] will be added to the signer set
/// of the node.
#[arg(long = "private-keys-count", default_value_t = 30)]
pub private_keys_to_add: usize,
/// The differential testing leader node implementation.
#[arg(short, long = "leader", default_value = "geth")]
pub leader: TestingPlatform,
@@ -129,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())
}
}
-1
View File
@@ -13,7 +13,6 @@ name = "retester"
path = "src/main.rs"
[dependencies]
revive-dt-common = { workspace = true }
revive-dt-compiler = { workspace = true }
revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true }
+63 -87
View File
@@ -1,15 +1,14 @@
//! The test driver handles the compilation and execution of the test cases.
use std::collections::HashMap;
use std::fmt::Debug;
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::{
CallFrame, GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingOptions, GethTrace,
PreStateConfig,
DefaultFrame, GethDebugTracingOptions, GethDefaultTracingOptions, GethTrace, PreStateConfig,
};
use alloy::{
primitives::Address,
@@ -20,21 +19,20 @@ use alloy::{
};
use anyhow::Context;
use indexmap::IndexMap;
use serde_json::Value;
use revive_dt_common::iterators::FilesWithExtensionIterator;
use revive_dt_compiler::{Compiler, SolidityCompiler};
use revive_dt_config::Arguments;
use revive_dt_format::case::CaseIdx;
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::Node;
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::fmt::Debug;
use crate::Platform;
use crate::common::*;
pub struct State<'a, T: Platform> {
/// The configuration that the framework was started with.
@@ -251,12 +249,6 @@ where
let tx = {
let tx = TransactionRequest::default().from(input.caller);
let tx = match input.value {
Some(ref value) if deploy_with_constructor_arguments => {
tx.value(value.into_inner())
}
_ => tx,
};
TransactionBuilder::<Ethereum>::with_deploy_code(tx, code)
};
@@ -373,7 +365,23 @@ where
let _guard = span.enter();
// Resolving the `input.expected` into a series of expectations that we can then assert on.
let mut expectations = match input {
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)),
..
@@ -389,17 +397,6 @@ where
Input { expected: None, .. } => vec![ExpectedOutput::new().with_success()],
};
// 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"
if let Method::Deployer = &input.method {
for expectation in expectations.iter_mut() {
expectation.return_data = None;
}
}
// 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.
@@ -407,14 +404,12 @@ where
.trace_transaction(
execution_receipt,
GethDebugTracingOptions {
tracer: Some(GethDebugTracerType::BuiltInTracer(
GethDebugBuiltInTracerType::CallTracer,
)),
config: GethDefaultTracingOptions::default().with_enable_return_data(true),
..Default::default()
},
)?
.try_into_call_frame()
.expect("Impossible - we requested a callframe trace so we must get it back");
.try_into_default_frame()
.expect("Impossible. We can't request default tracing and get some other type back");
for expectation in expectations.iter() {
self.handle_input_expectation_item(
@@ -435,21 +430,23 @@ where
execution_receipt: &TransactionReceipt,
node: &T::Blockchain,
expectation: &ExpectedOutput,
tracing_result: &CallFrame,
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?
let deployed_contracts = self.deployed_contracts.entry(case_idx).or_default();
let chain_state_provider = node;
// Handling the receipt state assertion.
let expected = !expectation.exception;
let actual = execution_receipt.status();
if actual != expected {
tracing::error!(expected, actual, "Transaction status assertion failed",);
tracing::error!(
?execution_receipt,
expected,
actual,
"Transaction status assertion failed",
);
anyhow::bail!(
"Transaction status assertion failed - Expected {expected} but got {actual}",
);
@@ -457,16 +454,13 @@ where
// Handling the calldata assertion
if let Some(ref expected_calldata) = expectation.return_data {
let expected = expected_calldata;
let actual = &tracing_result.output.as_ref().unwrap_or_default();
if !expected.is_equivalent(actual, deployed_contracts, chain_state_provider)? {
tracing::error!(
?execution_receipt,
?expected,
%actual,
"Calldata assertion failed"
);
anyhow::bail!("Calldata assertion failed - Expected {expected:?} but got {actual}",);
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}",);
}
}
@@ -476,7 +470,12 @@ where
let expected = expected_events.len();
let actual = execution_receipt.logs().len();
if actual != expected {
tracing::error!(expected, actual, "Event count assertion failed",);
tracing::error!(
?execution_receipt,
expected,
actual,
"Event count assertion failed",
);
anyhow::bail!(
"Event count assertion failed - Expected {expected} but got {actual}",
);
@@ -492,6 +491,7 @@ where
let actual = actual_event.address();
if actual != expected {
tracing::error!(
?execution_receipt,
%expected,
%actual,
"Event emitter assertion failed",
@@ -503,34 +503,27 @@ where
}
// Handling the topics assertion.
for (expected, actual) in expected_event
.topics
.as_slice()
.iter()
.zip(actual_event.topics())
{
let expected = Calldata::Compound(vec![expected.clone()]);
if !expected.is_equivalent(
&actual.0,
deployed_contracts,
chain_state_provider,
)? {
tracing::error!(
?execution_receipt,
?expected,
?actual,
"Event topics assertion failed",
);
anyhow::bail!(
"Event topics assertion failed - Expected {expected:?} but got {actual:?}",
);
}
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;
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.is_equivalent(&actual.0, deployed_contracts, chain_state_provider)? {
if !expected.starts_with(actual) {
tracing::error!(
?execution_receipt,
?expected,
@@ -650,22 +643,6 @@ where
let tracing_span = tracing::info_span!("Handling metadata file");
let _guard = tracing_span.enter();
// We only execute this input if it's valid for the leader and the follower. Otherwise, we
// skip it with a warning.
if !self
.leader_node
.matches_target(self.metadata.targets.as_deref())
|| !self
.follower_node
.matches_target(self.metadata.targets.as_deref())
{
tracing::warn!(
targets = ?self.metadata.targets,
"Either the leader or follower node do not support the targets of the file"
);
return execution_result;
}
for mode in self.metadata.solc_modes() {
let tracing_span = tracing::info_span!("With solc mode", solc_mode = ?mode);
let _guard = tracing_span.enter();
@@ -709,7 +686,6 @@ where
// For cases if one of the inputs fail then we move on to the next case and we do NOT
// bail out of the whole thing.
'case_loop: for (case_idx, case) in self.metadata.cases.iter().enumerate() {
let tracing_span = tracing::info_span!(
"Handling case",
+3 -3
View File
@@ -5,17 +5,17 @@
use revive_dt_compiler::{SolidityCompiler, revive_resolc, solc};
use revive_dt_config::TestingPlatform;
use revive_dt_format::traits::ResolverApi;
use revive_dt_node::{Node, geth, kitchensink::KitchensinkNode};
use revive_dt_node::{geth, kitchensink::KitchensinkNode};
use revive_dt_node_interaction::EthereumNode;
pub mod common;
pub mod driver;
/// One platform can be tested differentially against another.
///
/// For this we need a blockchain node implementation and a compiler.
pub trait Platform {
type Blockchain: EthereumNode + Node + ResolverApi;
type Blockchain: EthereumNode;
type Compiler: SolidityCompiler;
/// Returns the matching [TestingPlatform] of the [revive_dt_config::Arguments].
+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!(),
}
+1 -1
View File
@@ -9,7 +9,7 @@ repository.workspace = true
rust-version.workspace = true
[dependencies]
revive-dt-common = { workspace = true }
revive-dt-node-interaction = { workspace = true }
alloy = { workspace = true }
alloy-primitives = { workspace = true }
+8 -17
View File
@@ -1,8 +1,7 @@
use serde::Deserialize;
use revive_dt_common::macros::define_wrapper_type;
use crate::{
define_wrapper_type,
input::{Expected, Input},
mode::Mode,
};
@@ -17,6 +16,12 @@ pub struct Case {
pub expected: Option<Expected>,
}
define_wrapper_type!(
/// A wrapper type for the index of test cases found in metadata file.
#[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();
@@ -26,15 +31,7 @@ impl Case {
.enumerate()
.map(move |(idx, mut input)| {
if idx + 1 == inputs_len {
if input.expected.is_none() {
input.expected = self.expected.clone();
}
// TODO: What does it mean for us to have an `expected` field on the case itself
// but the final input also has an expected field that doesn't match the one on
// the case? What are we supposed to do with that final expected field on the
// case?
input.expected = self.expected.clone();
input
} else {
input
@@ -42,9 +39,3 @@ impl Case {
})
}
}
define_wrapper_type!(
/// A wrapper type for the index of test cases found in metadata file.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CaseIdx(usize);
);
+115 -159
View File
@@ -7,14 +7,13 @@ use alloy::{
primitives::{Address, Bytes, U256},
rpc::types::TransactionRequest,
};
use alloy_primitives::{FixedBytes, utils::parse_units};
use alloy_primitives::B256;
use semver::VersionReq;
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use revive_dt_common::macros::define_wrapper_type;
use revive_dt_node_interaction::EthereumNode;
use crate::metadata::ContractInstance;
use crate::traits::ResolverApi;
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Input {
@@ -27,7 +26,7 @@ pub struct Input {
#[serde(default)]
pub calldata: Calldata,
pub expected: Option<Expected>,
pub value: Option<EtherValue>,
pub value: Option<String>,
pub storage: Option<HashMap<String, Calldata>>,
}
@@ -51,14 +50,14 @@ pub struct ExpectedOutput {
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Event {
pub address: Option<Address>,
pub topics: Vec<String>,
pub topics: Vec<B256>,
pub values: Calldata,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(untagged)]
pub enum Calldata {
Single(Bytes),
Single(String),
Compound(Vec<String>),
}
@@ -83,37 +82,6 @@ pub enum Method {
FunctionName(String),
}
define_wrapper_type!(
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EtherValue(U256);
);
impl Serialize for EtherValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
format!("{} wei", self.0).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for EtherValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let string = String::deserialize(deserializer)?;
let mut splitted = string.split(' ');
let (Some(value), Some(unit)) = (splitted.next(), splitted.next()) else {
return Err(serde::de::Error::custom("Failed to parse the value"));
};
let parsed = parse_units(value, unit.replace("eth", "ether"))
.map_err(|_| serde::de::Error::custom("Failed to parse units"))?
.into();
Ok(Self(parsed))
}
}
impl ExpectedOutput {
pub fn new() -> Self {
Default::default()
@@ -155,7 +123,7 @@ impl Calldata {
pub fn calldata(
&self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
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)?;
@@ -166,11 +134,11 @@ impl Calldata {
&self,
buffer: &mut Vec<u8>,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
chain_state_provider: &impl EthereumNode,
) -> anyhow::Result<()> {
match self {
Calldata::Single(bytes) => {
buffer.extend_from_slice(bytes);
Calldata::Single(string) => {
alloy::hex::decode_to_slice(string, buffer)?;
}
Calldata::Compound(items) => {
for (arg_idx, arg) in items.iter().enumerate() {
@@ -191,48 +159,10 @@ impl Calldata {
pub fn size_requirement(&self) -> usize {
match self {
Calldata::Single(single) => single.len(),
Calldata::Single(single) => (single.len() - 2) / 2,
Calldata::Compound(items) => items.len() * 32,
}
}
/// Checks if this [`Calldata`] is equivalent to the passed calldata bytes.
pub fn is_equivalent(
&self,
other: &[u8],
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
) -> anyhow::Result<bool> {
match self {
Calldata::Single(calldata) => Ok(calldata == other),
Calldata::Compound(items) => {
// Chunking the "other" calldata into 32 byte chunks since each
// one of the items in the compound calldata represents 32 bytes
for (this, other) in items.iter().zip(other.chunks(32)) {
// The matterlabs format supports wildcards and therefore we
// also need to support them.
if this == "*" {
continue;
}
let other = if other.len() < 32 {
let mut vec = other.to_vec();
vec.resize(32, 0);
std::borrow::Cow::Owned(vec)
} else {
std::borrow::Cow::Borrowed(other)
};
let this = resolve_argument(this, deployed_contracts, chain_state_provider)?;
let other = U256::from_be_slice(&other);
if this != other {
return Ok(false);
}
}
Ok(true)
}
}
}
}
impl Input {
@@ -250,7 +180,7 @@ impl Input {
pub fn encoded_input(
&self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
chain_state_provider: &impl EthereumNode,
) -> anyhow::Result<Bytes> {
match self.method {
Method::Deployer | Method::Fallback => {
@@ -317,14 +247,10 @@ impl Input {
pub fn legacy_transaction(
&self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
chain_state_provider: &impl EthereumNode,
) -> anyhow::Result<TransactionRequest> {
let input_data = self.encoded_input(deployed_contracts, chain_state_provider)?;
let transaction_request = TransactionRequest::default().from(self.caller).value(
self.value
.map(|value| value.into_inner())
.unwrap_or_default(),
);
let transaction_request = TransactionRequest::default();
match self.method {
Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)),
_ => Ok(transaction_request
@@ -343,14 +269,12 @@ impl Input {
}
}
fn default_instance() -> ContractInstance {
pub fn default_instance() -> ContractInstance {
ContractInstance::new_from("Test")
}
pub const fn default_caller() -> Address {
Address(FixedBytes(alloy::hex!(
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
)))
pub fn default_caller() -> Address {
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap()
}
/// This function takes in the string calldata argument provided in the JSON input and resolves it
@@ -364,7 +288,7 @@ pub const fn default_caller() -> Address {
fn resolve_argument(
value: &str,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl ResolverApi,
chain_state_provider: &impl EthereumNode,
) -> anyhow::Result<U256> {
if let Some(instance) = value.strip_suffix(".address") {
Ok(U256::from_be_slice(
@@ -433,9 +357,31 @@ mod tests {
use alloy_sol_types::SolValue;
use std::collections::HashMap;
struct MockResolver;
struct DummyEthereumNode;
impl EthereumNode for DummyEthereumNode {
fn execute_transaction(
&self,
_: TransactionRequest,
) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> {
unimplemented!()
}
fn trace_transaction(
&self,
_: &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,
) -> anyhow::Result<alloy::rpc::types::trace::geth::DiffMode> {
unimplemented!()
}
impl ResolverApi for MockResolver {
fn chain_id(&self) -> anyhow::Result<alloy_primitives::ChainId> {
Ok(0x123)
}
@@ -507,7 +453,7 @@ mod tests {
(Address::ZERO, parsed_abi),
);
let encoded = input.encoded_input(&contracts, &MockResolver).unwrap();
let encoded = input.encoded_input(&contracts, &DummyEthereumNode).unwrap();
assert!(encoded.0.starts_with(&selector));
type T = (u64,);
@@ -515,53 +461,6 @@ mod tests {
assert_eq!(decoded.0, 42);
}
#[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: "Contract".to_owned().into(),
method: Method::FunctionName("send(address)".to_owned()),
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, &MockResolver).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() {
let raw_abi = r#"[
@@ -598,7 +497,54 @@ mod tests {
(Address::ZERO, parsed_abi),
);
let encoded = input.encoded_input(&contracts, &MockResolver).unwrap();
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()
};
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,);
@@ -615,11 +561,11 @@ mod tests {
let input = "$CHAIN_ID";
// Act
let resolved = resolve_argument(input, &Default::default(), &MockResolver);
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(resolved, U256::from(MockResolver.chain_id().unwrap()))
assert_eq!(resolved, U256::from(DummyEthereumNode.chain_id().unwrap()))
}
#[test]
@@ -628,13 +574,17 @@ mod tests {
let input = "$GAS_LIMIT";
// Act
let resolved = resolve_argument(input, &Default::default(), &MockResolver);
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from(MockResolver.block_gas_limit(Default::default()).unwrap())
U256::from(
DummyEthereumNode
.block_gas_limit(Default::default())
.unwrap()
)
)
}
@@ -644,14 +594,14 @@ mod tests {
let input = "$COINBASE";
// Act
let resolved = resolve_argument(input, &Default::default(), &MockResolver);
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from_be_slice(
MockResolver
DummyEthereumNode
.block_coinbase(Default::default())
.unwrap()
.as_ref()
@@ -665,13 +615,15 @@ mod tests {
let input = "$DIFFICULTY";
// Act
let resolved = resolve_argument(input, &Default::default(), &MockResolver);
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
MockResolver.block_difficulty(Default::default()).unwrap()
DummyEthereumNode
.block_difficulty(Default::default())
.unwrap()
)
}
@@ -681,13 +633,13 @@ mod tests {
let input = "$BLOCK_HASH";
// Act
let resolved = resolve_argument(input, &Default::default(), &MockResolver);
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from_be_bytes(MockResolver.block_hash(Default::default()).unwrap().0)
U256::from_be_bytes(DummyEthereumNode.block_hash(Default::default()).unwrap().0)
)
}
@@ -697,13 +649,13 @@ mod tests {
let input = "$BLOCK_NUMBER";
// Act
let resolved = resolve_argument(input, &Default::default(), &MockResolver);
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from(MockResolver.last_block_number().unwrap())
U256::from(DummyEthereumNode.last_block_number().unwrap())
)
}
@@ -713,13 +665,17 @@ mod tests {
let input = "$BLOCK_TIMESTAMP";
// Act
let resolved = resolve_argument(input, &Default::default(), &MockResolver);
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from(MockResolver.block_timestamp(Default::default()).unwrap())
U256::from(
DummyEthereumNode
.block_timestamp(Default::default())
.unwrap()
)
)
}
}
+1 -1
View File
@@ -3,6 +3,6 @@
pub mod case;
pub mod corpus;
pub mod input;
pub mod macros;
pub mod metadata;
pub mod mode;
pub mod traits;
@@ -12,9 +12,11 @@
/// pub struct CaseId(usize);
/// ```
///
/// And would also implement a number of methods on this type making it easier to use.
/// And would also implement a number of methods on this type making it easier
/// to use.
///
/// These wrapper types become very useful as they make the code a lot easier to read.
/// These wrapper types become very useful as they make the code a lot easier
/// to read.
///
/// Take the following as an example:
///
@@ -24,28 +26,26 @@
/// }
/// ```
///
/// In the above code it's hard to understand what the various types refer to or what to expect them
/// to contain.
/// In the above code it's hard to understand what the various types refer to or
/// what to expect them to contain.
///
/// With these wrapper types we're able to create code that's self-documenting in that the types
/// tell us what the code is referring to. The above code is transformed into
/// With these wrapper types we're able to create code that's self-documenting
/// in that the types tell us what the code is referring to. The above code is
/// transformed into
///
/// ```rust,ignore
/// struct State {
/// contracts: HashMap<CaseId, HashMap<ContractName, ContractByteCode>>
/// }
/// ```
///
/// Note that we follow the same syntax for defining wrapper structs but we do not permit the use of
/// generics.
#[macro_export]
macro_rules! define_wrapper_type {
(
$(#[$meta: meta])*
$vis:vis struct $ident: ident($ty: ty);
$ident: ident($ty: ty) $(;)?
) => {
$(#[$meta])*
$vis struct $ident($ty);
pub struct $ident($ty);
impl $ident {
pub fn new(value: $ty) -> Self {
@@ -104,7 +104,3 @@ macro_rules! define_wrapper_type {
}
};
}
/// Technically not needed but this allows for the macro to be found in the `macros` module of the
/// crate in addition to being found in the root of the crate.
pub use define_wrapper_type;
+5 -11
View File
@@ -9,10 +9,9 @@ use std::{
use serde::{Deserialize, Serialize};
use revive_dt_common::macros::define_wrapper_type;
use crate::{
case::Case,
define_wrapper_type,
mode::{Mode, SolcMode},
};
@@ -45,7 +44,6 @@ impl Deref for MetadataFile {
#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)]
pub struct Metadata {
pub targets: Option<Vec<String>>,
pub cases: Vec<Case>,
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdentifier>>,
// TODO: Convert into wrapper types for clarity.
@@ -218,22 +216,18 @@ define_wrapper_type!(
/// Represents a contract instance found a metadata file.
///
/// Typically, this is used as the key to the "contracts" field of metadata files.
#[derive(
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ContractInstance(String);
ContractInstance(String);
);
define_wrapper_type!(
/// Represents a contract identifier found a metadata file.
///
/// A contract identifier is the name of the contract in the source code.
#[derive(
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ContractIdent(String);
ContractIdent(String);
);
/// Represents an identifier used for contracts.
-30
View File
@@ -1,30 +0,0 @@
use alloy::eips::BlockNumberOrTag;
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
use anyhow::Result;
/// A trait of the interface are required to implement to be used by the resolution logic that this
/// crate implements to go from string calldata and into the bytes calldata.
pub trait ResolverApi {
/// Returns the ID of the chain that the node is on.
fn chain_id(&self) -> Result<ChainId>;
// TODO: This is currently a u128 due to Kitchensink needing more than 64 bits for its gas limit
// when we implement the changes to the gas we need to adjust this to be a u64.
/// Returns the gas limit of the specified block.
fn block_gas_limit(&self, number: BlockNumberOrTag) -> Result<u128>;
/// Returns the coinbase of the specified block.
fn block_coinbase(&self, number: BlockNumberOrTag) -> Result<Address>;
/// Returns the difficulty of the specified block.
fn block_difficulty(&self, number: BlockNumberOrTag) -> Result<U256>;
/// Returns the hash of the specified block.
fn block_hash(&self, number: BlockNumberOrTag) -> Result<BlockHash>;
/// Returns the timestamp of the specified block,
fn block_timestamp(&self, number: BlockNumberOrTag) -> Result<BlockTimestamp>;
/// Returns the number of the last block.
fn last_block_number(&self) -> Result<BlockNumber>;
}
+4
View File
@@ -11,3 +11,7 @@ rust-version.workspace = true
[dependencies]
alloy = { workspace = true }
anyhow = { workspace = true }
futures = { workspace = true }
tracing = { workspace = true }
once_cell = { workspace = true }
tokio = { workspace = true }
@@ -9,7 +9,6 @@ use tokio::{
runtime::Builder,
sync::{mpsc::UnboundedSender, oneshot},
};
use tracing::Instrument;
/// A blocking async executor.
///
@@ -23,7 +22,7 @@ use tracing::Instrument;
/// executor to drive an async computation:
///
/// ```rust
/// use revive_dt_common::concepts::*;
/// use revive_dt_node_interaction::*;
///
/// fn blocking_function() {
/// let result = BlockingExecutor::execute(async move {
@@ -64,11 +63,6 @@ impl BlockingExecutor {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<TaskMessage>();
thread::spawn(move || {
tracing::info!(
thread_id = ?std::thread::current().id(),
"Starting async runtime thread"
);
let runtime = Builder::new_current_thread()
.enable_all()
.build()
@@ -113,9 +107,7 @@ impl BlockingExecutor {
// in the task message. In doing this conversion, we lose some of the type information since
// we're converting R => dyn Any. However, we will perform down-casting on the result to
// convert it back into R.
let future = Box::pin(
async move { Box::new(future.await) as Box<dyn Any + Send> }.in_current_span(),
);
let future = Box::pin(async move { Box::new(future.await) as Box<dyn Any + Send> });
let task = TaskMessage::new(future, response_tx);
if let Err(error) = STATE.tx.send(task) {
@@ -134,17 +126,22 @@ impl BlockingExecutor {
}
};
let result = match result {
Ok(result) => result,
match result.map(|result| {
*result
.downcast::<R>()
.expect("Type mismatch in the downcast")
}) {
Ok(result) => Ok(result),
Err(error) => {
tracing::error!(?error, "An error occurred when running the async task");
anyhow::bail!("An error occurred when running the async task: {error:?}")
tracing::error!(
?error,
"Failed to downcast the returned result into the expected type"
);
anyhow::bail!(
"Failed to downcast the returned result into the expected type: {error:?}"
)
}
};
Ok(*result
.downcast::<R>()
.expect("An error occurred when downcasting into R. This is a bug"))
}
}
}
/// Represents the state of the async runtime. This runtime is designed to be a singleton runtime
@@ -203,9 +200,7 @@ mod test {
fn panics_in_futures_are_caught() {
// Act
let result = BlockingExecutor::execute(async move {
panic!(
"If this panic causes, well, a panic, then this is an issue. If it's caught then all good!"
);
panic!("This is a panic!");
0xFFu8
});
+28
View File
@@ -1,9 +1,14 @@
//! This crate implements all node interactions.
use alloy::eips::BlockNumberOrTag;
use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256};
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
use alloy::rpc::types::{TransactionReceipt, TransactionRequest};
use anyhow::Result;
mod blocking_executor;
pub use blocking_executor::*;
/// An interface for all interactions with Ethereum compatible nodes.
pub trait EthereumNode {
/// Execute the [TransactionRequest] and return a [TransactionReceipt].
@@ -18,4 +23,27 @@ pub trait EthereumNode {
/// Returns the state diff of the transaction hash in the [TransactionReceipt].
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>;
// TODO: This is currently a u128 due to Kitchensink needing more than 64 bits for its gas limit
// when we implement the changes to the gas we need to adjust this to be a u64.
/// Returns the gas limit of the specified block.
fn block_gas_limit(&self, number: BlockNumberOrTag) -> Result<u128>;
/// Returns the coinbase of the specified block.
fn block_coinbase(&self, number: BlockNumberOrTag) -> Result<Address>;
/// Returns the difficulty of the specified block.
fn block_difficulty(&self, number: BlockNumberOrTag) -> Result<U256>;
/// Returns the hash of the specified block.
fn block_hash(&self, number: BlockNumberOrTag) -> Result<BlockHash>;
/// Returns the timestamp of the specified block,
fn block_timestamp(&self, number: BlockNumberOrTag) -> Result<BlockTimestamp>;
/// Returns the number of the last block.
fn last_block_number(&self) -> Result<BlockNumber>;
}
+1 -3
View File
@@ -14,10 +14,8 @@ alloy = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true }
revive-dt-common = { workspace = true }
revive-dt-config = { workspace = true }
revive-dt-format = { workspace = true }
revive-dt-node-interaction = { workspace = true }
revive-dt-config = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
+31 -1
View File
@@ -1,9 +1,14 @@
use ::core::pin::Pin;
use alloy::{
network::{Network, TransactionBuilder},
consensus::SignableTransaction,
network::{Network, TransactionBuilder, TxSigner},
primitives::Address,
providers::{
Provider, SendableTx,
fillers::{GasFiller, TxFiller},
},
signers::{Signature, local::PrivateKeySigner},
transports::TransportResult,
};
@@ -76,3 +81,28 @@ where
}
}
}
/// 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)
}
}
-5
View File
@@ -1,5 +0,0 @@
/// This constant defines how much Wei accounts are pre-seeded with in genesis.
///
/// Note: After changing this number, check that the tests for kitchensink work as we encountered
/// some issues with different values of the initial balance on Kitchensink.
pub const INITIAL_BALANCE: u128 = 10u128.pow(37);
+76 -75
View File
@@ -11,9 +11,9 @@ use std::{
use alloy::{
eips::BlockNumberOrTag,
genesis::{Genesis, GenesisAccount},
network::{Ethereum, EthereumWallet, NetworkWallet},
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, FixedBytes, U256},
genesis::Genesis,
network::{Ethereum, EthereumWallet},
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, U256},
providers::{
Provider, ProviderBuilder,
ext::DebugApi,
@@ -25,13 +25,14 @@ use alloy::{
},
signers::local::PrivateKeySigner,
};
use revive_dt_common::concepts::BlockingExecutor;
use revive_dt_config::Arguments;
use revive_dt_format::traits::ResolverApi;
use revive_dt_node_interaction::EthereumNode;
use revive_dt_node_interaction::{BlockingExecutor, EthereumNode};
use tracing::Level;
use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
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,
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
@@ -76,28 +80,31 @@ impl Instance {
const GETH_STDOUT_LOG_FILE_NAME: &str = "node_stdout.log";
const GETH_STDERR_LOG_FILE_NAME: &str = "node_stderr.log";
const TRANSACTION_INDEXING_ERROR: &str = "transaction indexing is in progress";
/// Create the node directory and call `geth init` to configure the genesis.
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn init(&mut self, genesis: String) -> anyhow::Result<&mut Self> {
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 signer_address in
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
{
// Note, the use of the entry API here means that we only modify the entries for any
// account that is not in the `alloc` field of the genesis state.
genesis
.alloc
.entry(signer_address)
.or_insert(GenesisAccount::default().with_balance(U256::from(INITIAL_BALANCE)));
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);
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")
.arg("--datadir")
@@ -271,45 +278,57 @@ impl EthereumNode for Instance {
// it eventually works, but we only do that if the error we get back is the "transaction
// indexing is in progress" error or if the receipt is None.
//
// Getting the transaction indexed and taking a receipt can take a long time especially
// when a lot of transactions are being submitted to the node. Thus, while initially we
// only allowed for 60 seconds of waiting with a 1 second delay in polling, we need to
// allow for a larger wait time. Therefore, in here we allow for 5 minutes of waiting
// with exponential backoff each time we attempt to get the receipt and find that it's
// not available.
// At the moment we do not allow for the 60 seconds to be modified and we take it as
// being an implementation detail that's invisible to anything outside of this module.
//
// We allow a total of 60 retries for getting the receipt with one second between each
// retry and the next which means that we allow for a total of 60 seconds of waiting
// before we consider that we're unable to get the transaction receipt.
let mut retries = 0;
let mut total_wait_duration = Duration::from_secs(0);
let max_allowed_wait_duration = Duration::from_secs(5 * 60);
loop {
if total_wait_duration >= max_allowed_wait_duration {
tracing::error!(
?total_wait_duration,
?max_allowed_wait_duration,
retry_count = retries,
"Failed to get receipt after polling for it"
);
anyhow::bail!(
"Polled for receipt for {total_wait_duration:?} but failed to get it"
);
}
match provider.get_transaction_receipt(*transaction_hash).await {
Ok(Some(receipt)) => break Ok(receipt),
Ok(None) => {}
Ok(Some(receipt)) => {
tracing::info!("Obtained the transaction receipt");
break Ok(receipt);
}
Ok(None) => {
if retries == 60 {
tracing::error!(
"Polled for transaction receipt for 60 seconds but failed to get it"
);
break Err(anyhow::anyhow!("Failed to get the transaction receipt"));
} else {
tracing::trace!(
retries,
"Sleeping for 1 second and trying to get the receipt again"
);
retries += 1;
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
continue;
}
}
Err(error) => {
let error_string = error.to_string();
if !error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
if error_string.contains("transaction indexing is in progress") {
if retries == 60 {
tracing::error!(
"Polled for transaction receipt for 60 seconds but failed to get it"
);
break Err(error.into());
} else {
tracing::trace!(
retries,
"Sleeping for 1 second and trying to get the receipt again"
);
retries += 1;
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
continue;
}
} else {
break Err(error.into());
}
}
};
let next_wait_duration = Duration::from_secs(2u64.pow(retries))
.min(max_allowed_wait_duration - total_wait_duration);
total_wait_duration += next_wait_duration;
retries += 1;
tokio::time::sleep(next_wait_duration).await;
}
}
})?
}
@@ -345,9 +364,7 @@ impl EthereumNode for Instance {
_ => anyhow::bail!("expected a diff mode trace"),
}
}
}
impl ResolverApi for Instance {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
let provider = self.provider();
@@ -431,20 +448,11 @@ impl ResolverApi 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());
let mut wallet = config.wallet();
for signer in (1..=config.private_keys_to_add)
.map(|id| U256::from(id))
.map(|id| id.to_be_bytes::<32>())
.map(|id| PrivateKeySigner::from_bytes(&FixedBytes(id)).unwrap())
{
wallet.register_signer(signer);
}
Self {
connection_string: base_directory.join(Self::IPC_FILE).display().to_string(),
data_directory: base_directory.join(Self::DATA_DIRECTORY),
@@ -455,11 +463,13 @@ impl Node for Instance {
handle: None,
network_id: config.network_id,
start_timeout: config.geth_start_timeout,
wallet,
wallet: config.wallet(),
// 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(),
}
}
@@ -508,14 +518,6 @@ impl Node for Instance {
.stdout;
Ok(String::from_utf8_lossy(&output).into())
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn matches_target(&self, targets: Option<&[String]>) -> bool {
match targets {
None => true,
Some(targets) => targets.iter().any(|str| str.as_str() == "evm"),
}
}
}
impl Drop for Instance {
@@ -528,7 +530,6 @@ impl Drop for Instance {
#[cfg(test)]
mod tests {
use revive_dt_config::Arguments;
use temp_dir::TempDir;
use crate::{GENESIS_JSON, Node};
@@ -545,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()
@@ -555,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}'"
+58 -80
View File
@@ -10,14 +10,12 @@ use std::{
use alloy::{
consensus::{BlockHeader, TxEnvelope},
eips::BlockNumberOrTag,
genesis::{Genesis, GenesisAccount},
hex,
network::{
Ethereum, EthereumWallet, Network, NetworkWallet, TransactionBuilder,
TransactionBuilderError, UnbuiltTransactionError,
},
primitives::{
Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, FixedBytes, U256,
Ethereum, EthereumWallet, Network, TransactionBuilder, TransactionBuilderError,
UnbuiltTransactionError,
},
primitives::{Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, U256},
providers::{
Provider, ProviderBuilder,
ext::DebugApi,
@@ -28,20 +26,17 @@ use alloy::{
eth::{Block, Header, Transaction},
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
},
signers::local::PrivateKeySigner,
};
use revive_dt_format::traits::ResolverApi;
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use sp_core::crypto::Ss58Codec;
use sp_runtime::AccountId32;
use tracing::Level;
use revive_dt_common::concepts::BlockingExecutor;
use revive_dt_config::Arguments;
use revive_dt_node_interaction::EthereumNode;
use revive_dt_node_interaction::{BlockingExecutor, EthereumNode};
use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
use crate::{Node, common::FallbackGasFiller};
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
@@ -128,20 +123,7 @@ impl KitchensinkNode {
None
})
.collect();
let mut eth_balances = {
let mut genesis = serde_json::from_str::<Genesis>(genesis)?;
for signer_address in
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
{
// Note, the use of the entry API here means that we only modify the entries for any
// account that is not in the `alloc` field of the genesis state.
genesis
.alloc
.entry(signer_address)
.or_insert(GenesisAccount::default().with_balance(U256::from(INITIAL_BALANCE)));
}
self.extract_balance_from_genesis_file(&genesis)?
};
let mut eth_balances = self.extract_balance_from_genesis_file(genesis)?;
merged_balances.append(&mut eth_balances);
chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] =
@@ -255,27 +237,42 @@ impl KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn extract_balance_from_genesis_file(
&self,
genesis: &Genesis,
genesis_str: &str,
) -> anyhow::Result<Vec<(String, u128)>> {
genesis
.alloc
.iter()
.try_fold(Vec::new(), |mut vec, (address, acc)| {
let substrate_address = Self::eth_to_substrate_address(address);
let balance = acc.balance.try_into()?;
vec.push((substrate_address, balance));
Ok(vec)
})
let genesis_json: JsonValue = serde_json::from_str(genesis_str)?;
let alloc = genesis_json
.get("alloc")
.and_then(|a| a.as_object())
.ok_or_else(|| anyhow::anyhow!("Missing 'alloc' in genesis"))?;
let mut balances = Vec::new();
for (eth_addr, obj) in alloc.iter() {
let balance_str = obj.get("balance").and_then(|b| b.as_str()).unwrap_or("0");
let balance = if balance_str.starts_with("0x") {
u128::from_str_radix(balance_str.trim_start_matches("0x"), 16)?
} else {
balance_str.parse::<u128>()?
};
let substrate_addr = Self::eth_to_substrate_address(eth_addr)?;
balances.push((substrate_addr.clone(), balance));
}
Ok(balances)
}
fn eth_to_substrate_address(address: &Address) -> String {
let eth_bytes = address.0.0;
fn eth_to_substrate_address(eth_addr: &str) -> anyhow::Result<String> {
let eth_bytes = hex::decode(eth_addr.trim_start_matches("0x"))?;
if eth_bytes.len() != 20 {
anyhow::bail!(
"Invalid Ethereum address length: expected 20 bytes, got {}",
eth_bytes.len()
);
}
let mut padded = [0xEEu8; 32];
padded[..20].copy_from_slice(&eth_bytes);
let account_id = AccountId32::from(padded);
account_id.to_ss58check()
Ok(account_id.to_ss58check())
}
fn wait_ready(logs_file_path: &Path, marker: &str, timeout: Duration) -> anyhow::Result<()> {
@@ -426,9 +423,7 @@ impl EthereumNode for KitchensinkNode {
_ => anyhow::bail!("expected a diff mode trace"),
}
}
}
impl ResolverApi for KitchensinkNode {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
let provider = self.provider();
@@ -512,27 +507,18 @@ impl ResolverApi 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());
let logs_directory = base_directory.join(Self::LOGS_DIRECTORY);
let mut wallet = config.wallet();
for signer in (1..=config.private_keys_to_add)
.map(|id| U256::from(id))
.map(|id| id.to_be_bytes::<32>())
.map(|id| PrivateKeySigner::from_bytes(&FixedBytes(id)).unwrap())
{
wallet.register_signer(signer);
}
Self {
id,
substrate_binary: config.kitchensink.clone(),
eth_proxy_binary: config.eth_proxy.clone(),
rpc_url: String::new(),
wallet,
wallet: config.wallet(),
base_directory,
logs_directory,
process_substrate: None,
@@ -593,14 +579,6 @@ impl Node for KitchensinkNode {
.stdout;
Ok(String::from_utf8_lossy(&output).into())
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn matches_target(&self, targets: Option<&[String]>) -> bool {
match targets {
None => true,
Some(targets) => targets.iter().any(|str| str.as_str() == "pvm"),
}
}
}
impl Drop for KitchensinkNode {
@@ -656,12 +634,6 @@ impl TransactionBuilder<KitchenSinkNetwork> for <Ethereum as Network>::Transacti
)
}
fn take_nonce(&mut self) -> Option<u64> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::take_nonce(
self,
)
}
fn input(&self) -> Option<&alloy::primitives::Bytes> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::input(self)
}
@@ -842,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)]
@@ -1082,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()
@@ -1137,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");
@@ -1151,12 +1129,12 @@ mod tests {
let contents = fs::read_to_string(&final_chainspec_path).expect("Failed to read chainspec");
// Validate that the Substrate addresses derived from the Ethereum addresses are in the file
let first_eth_addr = KitchensinkNode::eth_to_substrate_address(
&"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap(),
);
let second_eth_addr = KitchensinkNode::eth_to_substrate_address(
&"Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2".parse().unwrap(),
);
let first_eth_addr =
KitchensinkNode::eth_to_substrate_address("90F8bf6A479f320ead074411a4B0e7944Ea8c9C1")
.unwrap();
let second_eth_addr =
KitchensinkNode::eth_to_substrate_address("Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2")
.unwrap();
assert!(
contents.contains(&first_eth_addr),
@@ -1181,10 +1159,10 @@ mod tests {
}
"#;
let node = KitchensinkNode::new(&test_config().0);
let node = KitchensinkNode::new(&test_config().0, &[]);
let result = node
.extract_balance_from_genesis_file(&serde_json::from_str(genesis_json).unwrap())
.extract_balance_from_genesis_file(genesis_json)
.unwrap();
let result_map: std::collections::HashMap<_, _> = result.into_iter().collect();
@@ -1214,7 +1192,7 @@ mod tests {
];
for eth_addr in eth_addresses {
let ss58 = KitchensinkNode::eth_to_substrate_address(&eth_addr.parse().unwrap());
let ss58 = KitchensinkNode::eth_to_substrate_address(eth_addr).unwrap();
println!("Ethereum: {eth_addr} -> Substrate SS58: {ss58}");
}
@@ -1242,7 +1220,7 @@ mod tests {
];
for (eth_addr, expected_ss58) in cases {
let result = KitchensinkNode::eth_to_substrate_address(&eth_addr.parse().unwrap());
let result = KitchensinkNode::eth_to_substrate_address(eth_addr).unwrap();
assert_eq!(
result, expected_ss58,
"Mismatch for Ethereum address {eth_addr}"
@@ -1254,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();
}
@@ -1262,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!(
@@ -1275,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!(
+2 -6
View File
@@ -1,10 +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 constants;
pub mod geth;
pub mod kitchensink;
pub mod pool;
@@ -15,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.
///
@@ -32,8 +32,4 @@ pub trait Node: EthereumNode {
/// Returns the node version.
fn version(&self) -> anyhow::Result<String>;
/// Given a list of targets from the metadata file, this function determines if the metadata
/// file can be ran on this node or not.
fn matches_target(&self, targets: Option<&[String]>) -> bool;
}
+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)
+5 -1
View File
@@ -33,5 +33,9 @@
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp": "0x00",
"alloc": {}
"alloc": {
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1": {
"balance": "1000000000000000000"
}
}
}