Compare commits

...

41 Commits

Author SHA1 Message Date
Omar Abdulla d04cdf337f Merge remote-tracking branch 'origin/main' into cleanup-execution-logic 2025-07-18 14:47:22 +03:00
Omar Abdulla 11b568a442 Merge remote-tracking branch 'origin/main' into cleanup-execution-logic 2025-07-17 19:46:27 +03:00
Omar Abdulla 74b5e34260 Cleanup execution logic 2025-07-16 18:21:14 +03:00
Omar Abdulla 4c55bba53d Merge branch 'feature/better-input-parser' into omar-temp 2025-07-16 15:34:28 +03:00
Omar Abdulla 222b5d4f86 Merge branch 'bugfix/finding-contract-abi' into omar-temp 2025-07-16 15:34:21 +03:00
Omar Abdulla 762288bf04 Merge branch 'bugfix/kitchensink-gas-limit' into omar-temp 2025-07-16 15:34:04 +03:00
Omar Abdulla cac2220188 Merge remote-tracking branch 'origin/main' into feature/better-input-parser 2025-07-16 15:31:33 +03:00
Omar Abdulla 4bf22f2d2b Merge remote-tracking branch 'origin/main' into bugfix/finding-contract-abi 2025-07-16 15:21:21 +03:00
Omar Abdulla 5c64de7e67 Change kitchensink gas limit assertion 2025-07-15 14:08:55 +03:00
Omar Abdulla fa4bf95091 Add comment on alternative solutions 2025-07-15 13:57:31 +03:00
Omar Abdulla 2537a132e6 Fix tests 2025-07-15 13:54:58 +03:00
Omar Abdulla 20da99784e Add resolution logic for other matterlabs variables 2025-07-14 23:51:59 +03:00
Omar Abdulla 6d7cd67931 Expose APIs for getting the info of a specific block 2025-07-14 23:21:53 +03:00
Omar Abdulla 68bda92465 Add a way to get block info from the node 2025-07-14 23:14:37 +03:00
Omar Abdulla ddd775d703 Add a way to get the block difficulty from the node 2025-07-14 22:53:40 +03:00
Omar Abdulla 02547b62ee Add a way to get the coinbase address 2025-07-14 22:48:45 +03:00
Omar Abdulla 61540741e1 Add support for getting the gas limit from the node 2025-07-14 22:37:50 +03:00
Omar Abdulla fa4bbbb987 Use provider method in tests 2025-07-14 22:33:09 +03:00
Omar Abdulla 8f80b1da8a Get kitchensink provider to use kitchensink network 2025-07-14 22:30:30 +03:00
Omar Abdulla c6d63255ec Merge remote-tracking branch 'origin/bugfix/kitchensink-gas-limit' into feature/better-input-parser 2025-07-14 22:29:34 +03:00
Omar Abdulla a4f5c4c8af Add ability to get the chain_id from node 2025-07-14 22:16:37 +03:00
Omar Abdulla 7d48d1600e Give nodes a standard way to get their alloy provider 2025-07-14 21:59:44 +03:00
Omar Abdulla e7e00a50dd Merge branch 'bugfix/argument-encoding' into feature/better-input-parser 2025-07-14 21:37:09 +03:00
Omar Abdulla 27a0a0de0b Fix doc test 2025-07-14 21:33:57 +03:00
Omar Abdulla 83c20b1be3 Fix tests 2025-07-14 21:30:35 +03:00
Omar Abdulla 331705134a Update the async runtime with syntactic sugar. 2025-07-14 21:13:58 +03:00
Omar Abdulla 075c8235a7 Merge remote-tracking branch 'origin/main' into bugfix/argument-encoding 2025-07-14 20:36:42 +03:00
Omar Abdulla 5f86ade1e0 Implement ABI fix in the compiler trait impl 2025-07-14 20:31:06 +03:00
Omar Abdulla 43064022e8 Merge remote-tracking branch 'origin/main' into bugfix/finding-contract-abi 2025-07-14 20:25:08 +03:00
Omar Abdulla 57bb015fa3 Merge remote-tracking branch 'origin/main' into bugfix/kitchensink-gas-limit 2025-07-14 19:31:34 +03:00
Omar Abdulla 43e0d0e592 Remove reliance on the web3 crate 2025-07-14 18:27:38 +03:00
Omar 332012754d Merge pull request #35 from paritytech/bugfix/fix-kitchensink-no-advance
Fix an issue where kitchensink won't advance
2025-07-14 17:54:58 +03:00
Omar Abdulla eb6c64c17a Merge remote-tracking branch 'origin/main' into bugfix/kitchensink-gas-limit 2025-07-14 13:27:40 +03:00
Omar Abdulla 2373872230 Avoid extra buffer allocation 2025-07-14 00:02:48 +03:00
Omar Abdulla e3723e780a Fix function selector and argument encoding 2025-07-13 19:52:06 +03:00
Omar Abdulla 4d4398f83e Fix the ABI finding logic 2025-07-13 15:59:23 +03:00
Omar Abdulla 76c85f191c fix clippy warning 2025-07-11 17:31:42 +03:00
Omar Abdulla 7664e9735e fix clippy warning 2025-07-11 17:20:36 +03:00
Omar Abdulla 4bab457114 Added --dev to substrate-node arguments.
This commit adds the `--dev` argument to the `substrate-node` to allow
the chain to keep advancing as time goes own. We have found that if this
option is not added then the chain won't advance forward.
2025-07-11 17:18:42 +03:00
Omar Abdulla f6374ad52a fix formatting 2025-07-11 14:49:05 +03:00
Omar Abdulla abba0cee08 Introduce a custom kitchensink network 2025-07-11 11:26:55 +03:00
9 changed files with 1156 additions and 173 deletions
Generated
+1
View File
@@ -3984,6 +3984,7 @@ dependencies = [
"alloy-primitives", "alloy-primitives",
"alloy-sol-types", "alloy-sol-types",
"anyhow", "anyhow",
"revive-dt-node-interaction",
"semver 1.0.26", "semver 1.0.26",
"serde", "serde",
"serde_json", "serde_json",
+387 -42
View File
@@ -1,7 +1,7 @@
//! The test driver handles the compilation and execution of the test cases. //! The test driver handles the compilation and execution of the test cases.
use alloy::json_abi::JsonAbi; use alloy::json_abi::JsonAbi;
use alloy::network::TransactionBuilder; use alloy::network::{Ethereum, TransactionBuilder};
use alloy::rpc::types::TransactionReceipt; use alloy::rpc::types::TransactionReceipt;
use alloy::rpc::types::trace::geth::GethTrace; use alloy::rpc::types::trace::geth::GethTrace;
use alloy::{ use alloy::{
@@ -19,7 +19,7 @@ use revive_dt_report::reporter::{CompilationTask, Report, Span};
use revive_solc_json_interface::SolcStandardJsonOutput; use revive_solc_json_interface::SolcStandardJsonOutput;
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap as StdHashMap; use std::collections::HashMap as StdHashMap;
use tracing::Level; use std::fmt::Debug;
use crate::Platform; use crate::Platform;
@@ -134,8 +134,12 @@ where
std::any::type_name::<T>() std::any::type_name::<T>()
); );
let tx = let tx = match input.legacy_transaction(
match input.legacy_transaction(nonce, &self.deployed_contracts, &self.deployed_abis) { nonce,
&self.deployed_contracts,
&self.deployed_abis,
node,
) {
Ok(tx) => { Ok(tx) => {
tracing::debug!("Legacy transaction data: {tx:#?}"); tracing::debug!("Legacy transaction data: {tx:#?}");
tx tx
@@ -253,10 +257,12 @@ where
return Err(error.into()); return Err(error.into());
} }
}; };
let tx = {
let tx = TransactionRequest::default() let tx = TransactionRequest::default()
.nonce(nonce) .nonce(nonce)
.from(input.caller) .from(input.caller);
.with_deploy_code(code); TransactionBuilder::<Ethereum>::with_deploy_code(tx, code)
};
let receipt = match node.execute_transaction(tx) { let receipt = match node.execute_transaction(tx) {
Ok(receipt) => receipt, Ok(receipt) => receipt,
@@ -398,67 +404,178 @@ where
} }
} }
pub fn execute(&mut self, span: Span) -> anyhow::Result<()> { // A note on this function and the choice of how we handle errors that happen here. This is not
// a doc comment since it's a comment for the maintainers of this code and not for the users of
// this code.
//
// This function does a few things: it builds the contracts for the various SOLC modes needed.
// It deploys the contracts to the chain, and it executes the various inputs that are specified
// for the test cases.
//
// In most functions in the codebase, it's fine to just say "If we encounter an error just
// bubble it up to the caller", but this isn't a good idea to do here and we need an elaborate
// way to report errors all while being graceful and continuing execution where we can. For
// example, if one of the inputs of one of the cases fail to execute, then we should not just
// bubble that error up immediately. Instead, we should note it down and continue to the next
// case as the next case might succeed.
//
// Therefore, this method returns an `ExecutionResult` object, and not just a normal `Result`.
// This object is fully typed to contain information about what exactly in the execution was a
// success and what failed.
//
// The above then allows us to have better logging and better information in the caller of this
// function as we have a more detailed view of what worked and what didn't.
pub fn execute(&mut self, span: Span) -> ExecutionResult {
// This is the execution result object that all of the execution information will be
// collected into and returned at the end of the execution.
let mut execution_result = ExecutionResult::default();
let tracing_span = tracing::info_span!("Handling metadata file");
let _guard = tracing_span.enter();
for mode in self.metadata.solc_modes() { for mode in self.metadata.solc_modes() {
let tracing_span = tracing::info_span!("With solc mode", solc_mode = ?mode);
let _guard = tracing_span.enter();
let mut leader_state = State::<L>::new(self.config, span); let mut leader_state = State::<L>::new(self.config, span);
leader_state.build_contracts(&mode, self.metadata)?;
let mut follower_state = State::<F>::new(self.config, span); let mut follower_state = State::<F>::new(self.config, span);
follower_state.build_contracts(&mode, self.metadata)?;
for (case_idx, case) in self.metadata.cases.iter().enumerate() { // We build the contracts. If building the contracts for the metadata file fails then we
// Creating a tracing span to know which case within the metadata is being executed // have no other option but to keep note of this error and move on to the next solc mode
// and which one we're getting logs for. // and NOT just bail out of the execution as a whole.
let tracing_span = tracing::span!( let build_result = tracing::info_span!("Building contracts").in_scope(|| {
Level::INFO, match leader_state.build_contracts(&mode, self.metadata) {
"Executing case", Ok(_) => {
case = case.name, tracing::debug!(target = ?Target::Leader, "Contract building succeeded");
execution_result.add_successful_build(Target::Leader, mode.clone());
},
Err(error) => {
tracing::error!(target = ?Target::Leader, ?error, "Contract building failed");
execution_result.add_failed_build(Target::Leader, mode.clone(), error);
return Err(());
}
}
match follower_state.build_contracts(&mode, self.metadata) {
Ok(_) => {
tracing::debug!(target = ?Target::Follower, "Contract building succeeded");
execution_result.add_successful_build(Target::Follower, mode.clone());
},
Err(error) => {
tracing::error!(target = ?Target::Follower, ?error, "Contract building failed");
execution_result.add_failed_build(Target::Follower, mode.clone(), error);
return Err(());
}
}
Ok(())
});
if build_result.is_err() {
// Note: We skip to the next solc mode as there's nothing that we can do at this
// point, the building has failed. We do NOT bail out of the execution as a whole.
continue;
}
// 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",
case_name = case.name,
case_idx = case_idx case_idx = case_idx
); );
let _guard = tracing_span.enter(); let _guard = tracing_span.enter();
for input in &case.inputs { // For inputs if one of the inputs fail we move on to the next case (we do not move
tracing::debug!("Starting deploying contract {}", &input.instance); // on to the next input as it doesn't make sense. It depends on the previous one).
if let Err(err) = leader_state.deploy_contracts(input, self.leader_node) { for (input_idx, input) in case.inputs.iter().enumerate() {
tracing::error!("Leader deployment failed for {}: {err}", input.instance); let tracing_span = tracing::info_span!("Handling input", input_idx);
continue; let _guard = tracing_span.enter();
} else {
tracing::debug!("Leader deployment succeeded for {}", &input.instance); // TODO: verify if this is correct, I doubt that we need to do contract redeploy
// for each input. It doesn't quite look to be correct but we need to cross
// check with the matterlabs implementation. This matches our implementation but
// I have doubts around its correctness.
let deployment_result = tracing::info_span!(
"Deploying contracts",
contract_name = input.instance
)
.in_scope(|| {
if let Err(error) = leader_state.deploy_contracts(input, self.leader_node) {
tracing::error!(target = ?Target::Leader, ?error, "Contract deployment failed");
execution_result.add_failed_case(
Target::Leader,
mode.clone(),
case.name.clone().unwrap_or("no case name".to_owned()),
case_idx,
input_idx,
anyhow::Error::msg(
format!("Failed to deploy contracts, {error}")
)
);
return Err(error)
};
if let Err(error) =
follower_state.deploy_contracts(input, self.follower_node)
{
tracing::error!(target = ?Target::Follower, ?error, "Contract deployment failed");
execution_result.add_failed_case(
Target::Follower,
mode.clone(),
case.name.clone().unwrap_or("no case name".to_owned()),
case_idx,
input_idx,
anyhow::Error::msg(
format!("Failed to deploy contracts, {error}")
)
);
return Err(error)
};
Ok(())
});
if deployment_result.is_err() {
// Noting it again here: if something in the input fails we do not move on
// to the next input, we move to the next case completely.
continue 'case_loop;
} }
if let Err(err) = follower_state.deploy_contracts(input, self.follower_node) { let execution_result =
tracing::error!("Follower deployment failed for {}: {err}", input.instance); tracing::info_span!("Executing input", contract_name = input.instance)
continue; .in_scope(|| {
} else {
tracing::debug!("Follower deployment succeeded for {}", &input.instance);
}
tracing::debug!("Starting executing contract {}", &input.instance);
let (leader_receipt, _, leader_diff) = let (leader_receipt, _, leader_diff) =
match leader_state.execute_input(input, self.leader_node) { match leader_state.execute_input(input, self.leader_node) {
Ok(result) => result, Ok(result) => result,
Err(err) => { Err(error) => {
tracing::error!( tracing::error!(
"Leader execution failed for {}: {err}", target = ?Target::Leader,
input.instance ?error,
"Contract execution failed"
); );
continue; return Err(error);
} }
}; };
let (follower_receipt, _, follower_diff) = let (follower_receipt, _, follower_diff) =
match follower_state.execute_input(input, self.follower_node) { match follower_state.execute_input(input, self.follower_node) {
Ok(result) => result, Ok(result) => result,
Err(err) => { Err(error) => {
tracing::error!( tracing::error!(
"Follower execution failed for {}: {err}", target = ?Target::Follower,
input.instance ?error,
"Contract execution failed"
); );
continue; return Err(error);
} }
}; };
Ok((leader_receipt, leader_diff, follower_receipt, follower_diff))
});
let Ok((leader_receipt, leader_diff, follower_receipt, follower_diff)) =
execution_result
else {
// Noting it again here: if something in the input fails we do not move on
// to the next input, we move to the next case completely.
continue 'case_loop;
};
if leader_diff == follower_diff { if leader_diff == follower_diff {
tracing::debug!("State diffs match between leader and follower."); tracing::debug!("State diffs match between leader and follower.");
} else { } else {
@@ -481,10 +598,238 @@ where
); );
} }
} }
// Note: Only consider the case as having been successful after we have processed
// all of the inputs and completed the entire loop over the input.
execution_result.add_successful_case(
Target::Leader,
mode.clone(),
case.name.clone().unwrap_or("no case name".to_owned()),
case_idx,
);
execution_result.add_successful_case(
Target::Follower,
mode.clone(),
case.name.clone().unwrap_or("no case name".to_owned()),
case_idx,
);
} }
} }
Ok(()) execution_result
}
}
#[derive(Debug, Default)]
pub struct ExecutionResult {
pub results: Vec<Box<dyn ExecutionResultItem>>,
pub successful_cases_count: usize,
pub failed_cases_count: usize,
}
impl ExecutionResult {
pub fn new() -> Self {
Self {
results: Default::default(),
successful_cases_count: Default::default(),
failed_cases_count: Default::default(),
}
}
pub fn add_successful_build(&mut self, target: Target, solc_mode: SolcMode) {
self.results
.push(Box::new(BuildResult::Success { target, solc_mode }));
}
pub fn add_failed_build(&mut self, target: Target, solc_mode: SolcMode, error: anyhow::Error) {
self.results.push(Box::new(BuildResult::Failure {
target,
solc_mode,
error,
}));
}
pub fn add_successful_case(
&mut self,
target: Target,
solc_mode: SolcMode,
case_name: String,
case_idx: usize,
) {
self.successful_cases_count += 1;
self.results.push(Box::new(CaseResult::Success {
target,
solc_mode,
case_name,
case_idx,
}));
}
pub fn add_failed_case(
&mut self,
target: Target,
solc_mode: SolcMode,
case_name: String,
case_idx: usize,
input_idx: usize,
error: anyhow::Error,
) {
self.failed_cases_count += 1;
self.results.push(Box::new(CaseResult::Failure {
target,
solc_mode,
case_name,
case_idx,
error,
input_idx,
}));
}
}
pub trait ExecutionResultItem: Debug {
/// Converts this result item into an [`anyhow::Result`].
fn as_result(&self) -> Result<(), &anyhow::Error>;
/// Provides information on whether the provided result item is of a success or failure.
fn is_success(&self) -> bool;
/// Provides information of the target that this result is for.
fn target(&self) -> &Target;
/// Provides information on the [`SolcMode`] mode that we being used for this result item.
fn solc_mode(&self) -> &SolcMode;
/// Provides information on the case name and number that this result item pertains to. This is
/// [`None`] if the error doesn't belong to any case (e.g., if it's a build error outside of any
/// of the cases.).
fn case_name_and_index(&self) -> Option<(&str, usize)>;
/// Provides information on the input number that this result item pertains to. This is [`None`]
/// if the error doesn't belong to any input (e.g., if it's a build error outside of any of the
/// inputs.).
fn input_index(&self) -> Option<usize>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Target {
Leader,
Follower,
}
#[derive(Debug)]
pub enum BuildResult {
Success {
target: Target,
solc_mode: SolcMode,
},
Failure {
target: Target,
solc_mode: SolcMode,
error: anyhow::Error,
},
}
impl ExecutionResultItem for BuildResult {
fn as_result(&self) -> Result<(), &anyhow::Error> {
match self {
Self::Success { .. } => Ok(()),
Self::Failure { error, .. } => Err(error)?,
}
}
fn is_success(&self) -> bool {
match self {
Self::Success { .. } => true,
Self::Failure { .. } => false,
}
}
fn target(&self) -> &Target {
match self {
Self::Success { target, .. } | Self::Failure { target, .. } => target,
}
}
fn solc_mode(&self) -> &SolcMode {
match self {
Self::Success { solc_mode, .. } | Self::Failure { solc_mode, .. } => solc_mode,
}
}
fn case_name_and_index(&self) -> Option<(&str, usize)> {
None
}
fn input_index(&self) -> Option<usize> {
None
}
}
#[derive(Debug)]
pub enum CaseResult {
Success {
target: Target,
solc_mode: SolcMode,
case_name: String,
case_idx: usize,
},
Failure {
target: Target,
solc_mode: SolcMode,
case_name: String,
case_idx: usize,
input_idx: usize,
error: anyhow::Error,
},
}
impl ExecutionResultItem for CaseResult {
fn as_result(&self) -> Result<(), &anyhow::Error> {
match self {
Self::Success { .. } => Ok(()),
Self::Failure { error, .. } => Err(error)?,
}
}
fn is_success(&self) -> bool {
match self {
Self::Success { .. } => true,
Self::Failure { .. } => false,
}
}
fn target(&self) -> &Target {
match self {
Self::Success { target, .. } | Self::Failure { target, .. } => target,
}
}
fn solc_mode(&self) -> &SolcMode {
match self {
Self::Success { solc_mode, .. } | Self::Failure { solc_mode, .. } => solc_mode,
}
}
fn case_name_and_index(&self) -> Option<(&str, usize)> {
match self {
Self::Success {
case_name,
case_idx,
..
}
| Self::Failure {
case_name,
case_idx,
..
} => Some((case_name, *case_idx)),
}
}
fn input_index(&self) -> Option<usize> {
match self {
CaseResult::Success { .. } => None,
CaseResult::Failure { input_idx, .. } => Some(*input_idx),
}
} }
} }
+16 -11
View File
@@ -13,7 +13,7 @@ use revive_dt_node::pool::NodePool;
use revive_dt_report::reporter::{Report, Span}; use revive_dt_report::reporter::{Report, Span};
use temp_dir::TempDir; use temp_dir::TempDir;
use tracing::Level; use tracing::Level;
use tracing_subscriber::{EnvFilter, FmtSubscriber, fmt::format::FmtSpan}; use tracing_subscriber::{EnvFilter, FmtSubscriber};
static TEMP_DIR: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap()); static TEMP_DIR: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
@@ -39,7 +39,7 @@ fn init_cli() -> anyhow::Result<Arguments> {
.with_thread_ids(true) .with_thread_ids(true)
.with_thread_names(true) .with_thread_names(true)
.with_env_filter(EnvFilter::from_default_env()) .with_env_filter(EnvFilter::from_default_env())
.with_span_events(FmtSpan::ENTER | FmtSpan::CLOSE) .with_ansi(false)
.pretty() .pretty()
.finish(); .finish();
tracing::subscriber::set_global_default(subscriber)?; tracing::subscriber::set_global_default(subscriber)?;
@@ -116,19 +116,24 @@ where
follower_nodes.round_robbin(), follower_nodes.round_robbin(),
); );
match driver.execute(span) { let execution_result = driver.execute(span);
Ok(_) => {
tracing::info!( tracing::info!(
"metadata {} success", case_success_count = execution_result.successful_cases_count,
metadata.directory().as_ref().unwrap().display() case_failure_count = execution_result.failed_cases_count,
"Execution completed"
); );
let mut error_count = 0;
for result in execution_result.results.iter() {
if !result.is_success() {
tracing::error!(execution_error = ?result, "Encountered an error");
error_count += 1;
} }
Err(error) => {
tracing::warn!(
"metadata {} failure: {error:?}",
metadata.file_path.as_ref().unwrap().display()
);
} }
if error_count == 0 {
tracing::info!("Execution succeeded");
} else {
tracing::info!("Execution failed");
} }
}, },
); );
+2
View File
@@ -9,6 +9,8 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
revive-dt-node-interaction = { workspace = true }
alloy = { workspace = true } alloy = { workspace = true }
alloy-primitives = { workspace = true } alloy-primitives = { workspace = true }
alloy-sol-types = { workspace = true } alloy-sol-types = { workspace = true }
+230 -24
View File
@@ -1,6 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use alloy::{ use alloy::{
eips::BlockNumberOrTag,
json_abi::JsonAbi, json_abi::JsonAbi,
network::TransactionBuilder, network::TransactionBuilder,
primitives::{Address, Bytes, U256}, primitives::{Address, Bytes, U256},
@@ -10,6 +11,8 @@ use semver::VersionReq;
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use revive_dt_node_interaction::EthereumNode;
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Input { pub struct Input {
#[serde(default = "default_caller")] #[serde(default = "default_caller")]
@@ -84,6 +87,7 @@ impl Input {
&self, &self,
deployed_abis: &HashMap<String, JsonAbi>, deployed_abis: &HashMap<String, JsonAbi>,
deployed_contracts: &HashMap<String, Address>, deployed_contracts: &HashMap<String, Address>,
chain_state_provider: &impl EthereumNode,
) -> anyhow::Result<Bytes> { ) -> anyhow::Result<Bytes> {
let Method::FunctionName(ref function_name) = self.method else { let Method::FunctionName(ref function_name) = self.method else {
return Ok(Bytes::default()); // fallback or deployer — no input return Ok(Bytes::default()); // fallback or deployer — no input
@@ -145,7 +149,7 @@ impl Input {
calldata.extend(function.selector().0); calldata.extend(function.selector().0);
for (arg_idx, arg) in calldata_args.iter().enumerate() { for (arg_idx, arg) in calldata_args.iter().enumerate() {
match resolve_argument(arg, deployed_contracts) { match resolve_argument(arg, deployed_contracts, chain_state_provider) {
Ok(resolved) => { Ok(resolved) => {
calldata.extend(resolved.to_be_bytes::<32>()); calldata.extend(resolved.to_be_bytes::<32>());
} }
@@ -165,8 +169,10 @@ impl Input {
nonce: u64, nonce: u64,
deployed_contracts: &HashMap<String, Address>, deployed_contracts: &HashMap<String, Address>,
deployed_abis: &HashMap<String, JsonAbi>, deployed_abis: &HashMap<String, JsonAbi>,
chain_state_provider: &impl EthereumNode,
) -> anyhow::Result<TransactionRequest> { ) -> anyhow::Result<TransactionRequest> {
let input_data = self.encoded_input(deployed_abis, deployed_contracts)?; let input_data =
self.encoded_input(deployed_abis, deployed_contracts, chain_state_provider)?;
let transaction_request = TransactionRequest::default().nonce(nonce); let transaction_request = TransactionRequest::default().nonce(nonce);
match self.method { match self.method {
Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)), Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)),
@@ -196,6 +202,7 @@ fn default_caller() -> Address {
fn resolve_argument( fn resolve_argument(
value: &str, value: &str,
deployed_contracts: &HashMap<String, Address>, deployed_contracts: &HashMap<String, Address>,
chain_state_provider: &impl EthereumNode,
) -> anyhow::Result<U256> { ) -> anyhow::Result<U256> {
if let Some(instance) = value.strip_suffix(".address") { if let Some(instance) = value.strip_suffix(".address") {
Ok(U256::from_be_slice( Ok(U256::from_be_slice(
@@ -217,32 +224,42 @@ fn resolve_argument(
} else if let Some(value) = value.strip_prefix("0x") { } else if let Some(value) = value.strip_prefix("0x") {
Ok(U256::from_str_radix(value, 16) Ok(U256::from_str_radix(value, 16)
.map_err(|error| anyhow::anyhow!("Invalid hexadecimal literal: {}", error))?) .map_err(|error| anyhow::anyhow!("Invalid hexadecimal literal: {}", error))?)
} else { } else if value == "$CHAIN_ID" {
// TODO: This is a set of "variables" that we need to be able to resolve to be fully in let chain_id = chain_state_provider.chain_id()?;
// compliance with the matter labs tester but we currently do not resolve them. We need to Ok(U256::from(chain_id))
// add logic that does their resolution in the future, perhaps through some kind of system } else if value == "$GAS_LIMIT" {
// context API that we pass down to the resolution function that allows it to make calls to let gas_limit = chain_state_provider.block_gas_limit(BlockNumberOrTag::Latest)?;
// the node to perform these resolutions. Ok(U256::from(gas_limit))
let is_unsupported = [ } else if value == "$COINBASE" {
"$CHAIN_ID", let coinbase = chain_state_provider.block_coinbase(BlockNumberOrTag::Latest)?;
"$GAS_LIMIT", Ok(U256::from_be_slice(coinbase.as_ref()))
"$COINBASE", } else if value == "$DIFFICULTY" {
"$DIFFICULTY", let block_difficulty = chain_state_provider.block_difficulty(BlockNumberOrTag::Latest)?;
"$BLOCK_HASH", Ok(block_difficulty)
"$BLOCK_TIMESTAMP", } else if value.starts_with("$BLOCK_HASH") {
] let offset: u64 = value
.iter() .split(':')
.any(|var| value.starts_with(var)); .next_back()
.and_then(|value| value.parse().ok())
.unwrap_or_default();
if is_unsupported { let current_block_number = chain_state_provider.last_block_number()?;
tracing::error!(value, "Unsupported variable used"); let desired_block_number = current_block_number - offset;
anyhow::bail!("Encountered {value} which is currently unsupported by the framework");
let block_hash = chain_state_provider.block_hash(desired_block_number.into())?;
Ok(U256::from_be_bytes(block_hash.0))
} else if value == "$BLOCK_NUMBER" {
let current_block_number = chain_state_provider.last_block_number()?;
Ok(U256::from(current_block_number))
} else if value == "$BLOCK_TIMESTAMP" {
let timestamp = chain_state_provider.block_timestamp(BlockNumberOrTag::Latest)?;
Ok(U256::from(timestamp))
} else { } else {
Ok(U256::from_str_radix(value, 10) Ok(U256::from_str_radix(value, 10)
.map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?) .map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?)
} }
} }
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@@ -253,6 +270,69 @@ mod tests {
use alloy_sol_types::SolValue; use alloy_sol_types::SolValue;
use std::collections::HashMap; use std::collections::HashMap;
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,
) -> 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!()
}
fn fetch_add_nonce(&self, _: Address) -> anyhow::Result<u64> {
unimplemented!()
}
fn chain_id(&self) -> anyhow::Result<alloy_primitives::ChainId> {
Ok(0x123)
}
fn block_gas_limit(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result<u128> {
Ok(0x1234)
}
fn block_coinbase(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result<Address> {
Ok(Address::ZERO)
}
fn block_difficulty(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result<U256> {
Ok(U256::from(0x12345u128))
}
fn block_hash(
&self,
_: alloy::eips::BlockNumberOrTag,
) -> anyhow::Result<alloy_primitives::BlockHash> {
Ok([0xEE; 32].into())
}
fn block_timestamp(
&self,
_: alloy::eips::BlockNumberOrTag,
) -> anyhow::Result<alloy_primitives::BlockTimestamp> {
Ok(0x123456)
}
fn last_block_number(&self) -> anyhow::Result<alloy_primitives::BlockNumber> {
Ok(0x1234567)
}
}
#[test] #[test]
fn test_encoded_input_uint256() { fn test_encoded_input_uint256() {
let raw_metadata = r#" let raw_metadata = r#"
@@ -288,7 +368,7 @@ mod tests {
let deployed_contracts = HashMap::new(); let deployed_contracts = HashMap::new();
let encoded = input let encoded = input
.encoded_input(&deployed_abis, &deployed_contracts) .encoded_input(&deployed_abis, &deployed_contracts, &DummyEthereumNode)
.unwrap(); .unwrap();
assert!(encoded.0.starts_with(&selector)); assert!(encoded.0.starts_with(&selector));
@@ -331,7 +411,9 @@ mod tests {
abis.insert("Contract".to_string(), parsed_abi); abis.insert("Contract".to_string(), parsed_abi);
let contracts = HashMap::new(); let contracts = HashMap::new();
let encoded = input.encoded_input(&abis, &contracts).unwrap(); let encoded = input
.encoded_input(&abis, &contracts, &DummyEthereumNode)
.unwrap();
assert!(encoded.0.starts_with(&selector)); assert!(encoded.0.starts_with(&selector));
type T = (alloy_primitives::Address,); type T = (alloy_primitives::Address,);
@@ -341,4 +423,128 @@ mod tests {
address!("0x1000000000000000000000000000000000000001") address!("0x1000000000000000000000000000000000000001")
); );
} }
#[test]
fn resolver_can_resolve_chain_id_variable() {
// Arrange
let input = "$CHAIN_ID";
// Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(resolved, U256::from(DummyEthereumNode.chain_id().unwrap()))
}
#[test]
fn resolver_can_resolve_gas_limit_variable() {
// Arrange
let input = "$GAS_LIMIT";
// Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from(
DummyEthereumNode
.block_gas_limit(Default::default())
.unwrap()
)
)
}
#[test]
fn resolver_can_resolve_coinbase_variable() {
// Arrange
let input = "$COINBASE";
// Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from_be_slice(
DummyEthereumNode
.block_coinbase(Default::default())
.unwrap()
.as_ref()
)
)
}
#[test]
fn resolver_can_resolve_block_difficulty_variable() {
// Arrange
let input = "$DIFFICULTY";
// Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
DummyEthereumNode
.block_difficulty(Default::default())
.unwrap()
)
}
#[test]
fn resolver_can_resolve_block_hash_variable() {
// Arrange
let input = "$BLOCK_HASH";
// Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from_be_bytes(DummyEthereumNode.block_hash(Default::default()).unwrap().0)
)
}
#[test]
fn resolver_can_resolve_block_number_variable() {
// Arrange
let input = "$BLOCK_NUMBER";
// Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from(DummyEthereumNode.last_block_number().unwrap())
)
}
#[test]
fn resolver_can_resolve_block_timestamp_variable() {
// Arrange
let input = "$BLOCK_TIMESTAMP";
// Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from(
DummyEthereumNode
.block_timestamp(Default::default())
.unwrap()
)
)
}
} }
@@ -144,7 +144,6 @@ impl BlockingExecutor {
} }
} }
} }
/// Represents the state of the async runtime. This runtime is designed to be a singleton runtime /// Represents the state of the async runtime. This runtime is designed to be a singleton runtime
/// which means that in the current running program there's just a single thread that has an async /// which means that in the current running program there's just a single thread that has an async
/// runtime. /// runtime.
+30 -8
View File
@@ -1,8 +1,10 @@
//! This crate implements all node interactions. //! This crate implements all node interactions.
use alloy::primitives::Address; 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, GethTrace};
use alloy::rpc::types::{TransactionReceipt, TransactionRequest}; use alloy::rpc::types::{TransactionReceipt, TransactionRequest};
use anyhow::Result;
mod blocking_executor; mod blocking_executor;
pub use blocking_executor::*; pub use blocking_executor::*;
@@ -10,17 +12,37 @@ pub use blocking_executor::*;
/// An interface for all interactions with Ethereum compatible nodes. /// An interface for all interactions with Ethereum compatible nodes.
pub trait EthereumNode { pub trait EthereumNode {
/// Execute the [TransactionRequest] and return a [TransactionReceipt]. /// Execute the [TransactionRequest] and return a [TransactionReceipt].
fn execute_transaction( fn execute_transaction(&self, transaction: TransactionRequest) -> Result<TransactionReceipt>;
&self,
transaction: TransactionRequest,
) -> anyhow::Result<TransactionReceipt>;
/// Trace the transaction in the [TransactionReceipt] and return a [GethTrace]. /// Trace the transaction in the [TransactionReceipt] and return a [GethTrace].
fn trace_transaction(&self, transaction: TransactionReceipt) -> anyhow::Result<GethTrace>; fn trace_transaction(&self, transaction: TransactionReceipt) -> Result<GethTrace>;
/// Returns the state diff of the transaction hash in the [TransactionReceipt]. /// Returns the state diff of the transaction hash in the [TransactionReceipt].
fn state_diff(&self, transaction: TransactionReceipt) -> anyhow::Result<DiffMode>; fn state_diff(&self, transaction: TransactionReceipt) -> Result<DiffMode>;
/// Returns the next available nonce for the given [Address]. /// Returns the next available nonce for the given [Address].
fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64>; fn fetch_add_nonce(&self, address: Address) -> Result<u64>;
/// 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>;
} }
+213 -22
View File
@@ -14,9 +14,14 @@ use std::{
}; };
use alloy::{ use alloy::{
network::EthereumWallet, eips::BlockNumberOrTag,
primitives::Address, network::{Ethereum, EthereumWallet},
providers::{Provider, ProviderBuilder, ext::DebugApi}, primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, U256},
providers::{
Provider, ProviderBuilder,
ext::DebugApi,
fillers::{FillProvider, TxFiller},
},
rpc::types::{ rpc::types::{
TransactionReceipt, TransactionRequest, TransactionReceipt, TransactionRequest,
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
@@ -191,6 +196,24 @@ impl Instance {
fn geth_stderr_log_file_path(&self) -> PathBuf { fn geth_stderr_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::GETH_STDERR_LOG_FILE_NAME) self.logs_directory.join(Self::GETH_STDERR_LOG_FILE_NAME)
} }
fn provider(
&self,
) -> impl Future<
Output = anyhow::Result<
FillProvider<impl TxFiller<Ethereum>, impl Provider<Ethereum>, Ethereum>,
>,
> + 'static {
let connection_string = self.connection_string();
let wallet = self.wallet.clone();
Box::pin(async move {
ProviderBuilder::new()
.wallet(wallet)
.connect(&connection_string)
.await
.map_err(Into::into)
})
}
} }
impl EthereumNode for Instance { impl EthereumNode for Instance {
@@ -199,17 +222,12 @@ impl EthereumNode for Instance {
&self, &self,
transaction: TransactionRequest, transaction: TransactionRequest,
) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> { ) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> {
let connection_string = self.connection_string(); let provider = self.provider();
let wallet = self.wallet.clone();
BlockingExecutor::execute(async move { 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 _outer_guard = outer_span.enter();
let provider = ProviderBuilder::new() let provider = provider.await?;
.wallet(wallet)
.connect(&connection_string)
.await?;
let pending_transaction = provider.send_transaction(transaction).await?; let pending_transaction = provider.send_transaction(transaction).await?;
let transaction_hash = pending_transaction.tx_hash(); let transaction_hash = pending_transaction.tx_hash();
@@ -289,18 +307,15 @@ impl EthereumNode for Instance {
&self, &self,
transaction: TransactionReceipt, transaction: TransactionReceipt,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> { ) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
let connection_string = self.connection_string();
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig { let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true), diff_mode: Some(true),
disable_code: None, disable_code: None,
disable_storage: None, disable_storage: None,
}); });
let wallet = self.wallet.clone(); let provider = self.provider();
BlockingExecutor::execute(async move { BlockingExecutor::execute(async move {
Ok(ProviderBuilder::new() Ok(provider
.wallet(wallet)
.connect(&connection_string)
.await? .await?
.debug_trace_transaction(transaction.transaction_hash, trace_options) .debug_trace_transaction(transaction.transaction_hash, trace_options)
.await?) .await?)
@@ -323,13 +338,9 @@ impl EthereumNode for Instance {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))] #[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64> { fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64> {
let connection_string = self.connection_string.clone(); let provider = self.provider();
let wallet = self.wallet.clone();
let onchain_nonce = BlockingExecutor::execute::<anyhow::Result<_>>(async move { let onchain_nonce = BlockingExecutor::execute::<anyhow::Result<_>>(async move {
ProviderBuilder::new() provider
.wallet(wallet)
.connect(&connection_string)
.await? .await?
.get_transaction_count(address) .get_transaction_count(address)
.await .await
@@ -342,6 +353,87 @@ impl EthereumNode for Instance {
*current += 1; *current += 1;
Ok(value) 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();
BlockingExecutor::execute(async move {
provider.await?.get_chain_id().await.map_err(Into::into)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.gas_limit as _)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.beneficiary)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.difficulty)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.hash)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.timestamp)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider.await?.get_block_number().await.map_err(Into::into)
})?
}
} }
impl Node for Instance { impl Node for Instance {
@@ -429,7 +521,7 @@ mod tests {
use crate::{GENESIS_JSON, Node}; use crate::{GENESIS_JSON, Node};
use super::Instance; use super::*;
fn test_config() -> (Arguments, TempDir) { fn test_config() -> (Arguments, TempDir) {
let mut config = Arguments::default(); let mut config = Arguments::default();
@@ -439,6 +531,16 @@ mod tests {
(config, temp_dir) (config, temp_dir)
} }
fn new_node() -> (Instance, TempDir) {
let (args, temp_dir) = test_config();
let mut node = Instance::new(&args);
node.init(GENESIS_JSON.to_owned())
.expect("Failed to initialize the node")
.spawn_process()
.expect("Failed to spawn the node process");
(node, temp_dir)
}
#[test] #[test]
fn init_works() { fn init_works() {
Instance::new(&test_config().0) Instance::new(&test_config().0)
@@ -461,4 +563,93 @@ mod tests {
"expected version string, got: '{version}'" "expected version string, got: '{version}'"
); );
} }
#[test]
fn can_get_chain_id_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
// Act
let chain_id = node.chain_id();
// Assert
let chain_id = chain_id.expect("Failed to get the chain id");
assert_eq!(chain_id, 420_420_420);
}
#[test]
fn can_get_gas_limit_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
// Act
let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest);
// Assert
let gas_limit = gas_limit.expect("Failed to get the gas limit");
assert_eq!(gas_limit, u32::MAX as u128)
}
#[test]
fn can_get_coinbase_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
// Act
let coinbase = node.block_coinbase(BlockNumberOrTag::Latest);
// Assert
let coinbase = coinbase.expect("Failed to get the coinbase");
assert_eq!(coinbase, Address::new([0xFF; 20]))
}
#[test]
fn can_get_block_difficulty_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
// Act
let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest);
// Assert
let block_difficulty = block_difficulty.expect("Failed to get the block difficulty");
assert_eq!(block_difficulty, U256::ZERO)
}
#[test]
fn can_get_block_hash_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
// Act
let block_hash = node.block_hash(BlockNumberOrTag::Latest);
// Assert
let _ = block_hash.expect("Failed to get the block hash");
}
#[test]
fn can_get_block_timestamp_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
// Act
let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest);
// Assert
let _ = block_timestamp.expect("Failed to get the block timestamp");
}
#[test]
fn can_get_block_number_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
// Act
let block_number = node.last_block_number();
// Assert
let block_number = block_number.expect("Failed to get the block number");
assert_eq!(block_number, 0)
}
} }
+247 -35
View File
@@ -13,13 +13,18 @@ use std::{
use alloy::{ use alloy::{
consensus::{BlockHeader, TxEnvelope}, consensus::{BlockHeader, TxEnvelope},
eips::BlockNumberOrTag,
hex, hex,
network::{ network::{
Ethereum, EthereumWallet, Network, TransactionBuilder, TransactionBuilderError, Ethereum, EthereumWallet, Network, TransactionBuilder, TransactionBuilderError,
UnbuiltTransactionError, UnbuiltTransactionError,
}, },
primitives::{Address, B64, B256, BlockNumber, Bloom, Bytes, U256}, primitives::{Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, U256},
providers::{Provider, ProviderBuilder, ext::DebugApi}, providers::{
Provider, ProviderBuilder,
ext::DebugApi,
fillers::{FillProvider, TxFiller},
},
rpc::types::{ rpc::types::{
TransactionReceipt, TransactionReceipt,
eth::{Block, Header, Transaction}, eth::{Block, Header, Transaction},
@@ -232,6 +237,7 @@ impl KitchensinkNode {
Ok(()) Ok(())
} }
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))] #[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn extract_balance_from_genesis_file( fn extract_balance_from_genesis_file(
&self, &self,
@@ -330,6 +336,29 @@ impl KitchensinkNode {
fn proxy_stderr_log_file_path(&self) -> PathBuf { fn proxy_stderr_log_file_path(&self) -> PathBuf {
self.logs_directory.join(Self::PROXY_STDERR_LOG_FILE_NAME) self.logs_directory.join(Self::PROXY_STDERR_LOG_FILE_NAME)
} }
fn provider(
&self,
) -> impl Future<
Output = anyhow::Result<
FillProvider<
impl TxFiller<KitchenSinkNetwork>,
impl Provider<KitchenSinkNetwork>,
KitchenSinkNetwork,
>,
>,
> + 'static {
let connection_string = self.connection_string();
let wallet = self.wallet.clone();
Box::pin(async move {
ProviderBuilder::new()
.network::<KitchenSinkNetwork>()
.wallet(wallet)
.connect(&connection_string)
.await
.map_err(Into::into)
})
}
} }
impl EthereumNode for KitchensinkNode { impl EthereumNode for KitchensinkNode {
@@ -338,17 +367,10 @@ impl EthereumNode for KitchensinkNode {
&self, &self,
transaction: alloy::rpc::types::TransactionRequest, transaction: alloy::rpc::types::TransactionRequest,
) -> anyhow::Result<TransactionReceipt> { ) -> anyhow::Result<TransactionReceipt> {
let url = self.rpc_url.clone(); tracing::debug!(?transaction, "Submitting transaction");
let wallet = self.wallet.clone(); let provider = self.provider();
tracing::debug!("Submitting transaction: {transaction:#?}");
tracing::info!("Submitting tx to kitchensink");
let receipt = BlockingExecutor::execute(async move { let receipt = BlockingExecutor::execute(async move {
Ok(ProviderBuilder::new() Ok(provider
.network::<KitchenSinkNetwork>()
.wallet(wallet)
.connect(&url)
.await? .await?
.send_transaction(transaction) .send_transaction(transaction)
.await? .await?
@@ -364,20 +386,15 @@ impl EthereumNode for KitchensinkNode {
&self, &self,
transaction: TransactionReceipt, transaction: TransactionReceipt,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> { ) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
let url = self.rpc_url.clone();
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig { let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true), diff_mode: Some(true),
disable_code: None, disable_code: None,
disable_storage: None, disable_storage: None,
}); });
let provider = self.provider();
let wallet = self.wallet.clone();
BlockingExecutor::execute(async move { BlockingExecutor::execute(async move {
Ok(ProviderBuilder::new() Ok(provider
.network::<KitchenSinkNetwork>()
.wallet(wallet)
.connect(&url)
.await? .await?
.debug_trace_transaction(transaction.transaction_hash, trace_options) .debug_trace_transaction(transaction.transaction_hash, trace_options)
.await?) .await?)
@@ -397,13 +414,9 @@ impl EthereumNode for KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))] #[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64> { fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64> {
let url = self.rpc_url.clone(); let provider = self.provider();
let wallet = self.wallet.clone();
let onchain_nonce = BlockingExecutor::execute::<anyhow::Result<_>>(async move { let onchain_nonce = BlockingExecutor::execute::<anyhow::Result<_>>(async move {
ProviderBuilder::new() provider
.wallet(wallet)
.connect(&url)
.await? .await?
.get_transaction_count(address) .get_transaction_count(address)
.await .await
@@ -416,6 +429,87 @@ impl EthereumNode for KitchensinkNode {
*current += 1; *current += 1;
Ok(value) 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();
BlockingExecutor::execute(async move {
provider.await?.get_chain_id().await.map_err(Into::into)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result<u128> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.gas_limit)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result<Address> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.beneficiary)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result<U256> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.difficulty)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockHash> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.hash)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result<BlockTimestamp> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider
.await?
.get_block_by_number(number)
.await?
.ok_or(anyhow::Error::msg("Blockchain has no blocks"))
.map(|block| block.header.timestamp)
})?
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn last_block_number(&self) -> anyhow::Result<BlockNumber> {
let provider = self.provider();
BlockingExecutor::execute(async move {
provider.await?.get_block_number().await.map_err(Into::into)
})?
}
} }
impl Node for KitchensinkNode { impl Node for KitchensinkNode {
@@ -926,6 +1020,7 @@ mod tests {
use alloy::rpc::types::TransactionRequest; use alloy::rpc::types::TransactionRequest;
use revive_dt_config::Arguments; use revive_dt_config::Arguments;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::LazyLock;
use temp_dir::TempDir; use temp_dir::TempDir;
use std::fs; use std::fs;
@@ -945,20 +1040,49 @@ mod tests {
(config, temp_dir) (config, temp_dir)
} }
fn new_node() -> (KitchensinkNode, Arguments, TempDir) {
// Note: When we run the tests in the CI we found that if they're all
// run in parallel then the CI is unable to start all of the nodes in
// time and their start up times-out. Therefore, we want all of the
// nodes to be started in series and not in parallel. To do this, we use
// a dummy mutex here such that there can only be a single node being
// started up at any point of time. This will make our tests run slower
// but it will allow the node startup to not timeout.
//
// Note: an alternative to starting all of the nodes in series and not
// in parallel would be for us to reuse the same node between tests
// which is not the best thing to do in my opinion as it removes all
// of the isolation between tests and makes them depend on what other
// tests do. For example, if one test checks what the block number is
// and another test submits a transaction then the tx test would have
// side effects that affect the block number test.
static NODE_START_MUTEX: Mutex<()> = Mutex::new(());
let _guard = NODE_START_MUTEX.lock().unwrap();
let (args, temp_dir) = test_config();
let mut node = KitchensinkNode::new(&args);
node.init(GENESIS_JSON)
.expect("Failed to initialize the node")
.spawn_process()
.expect("Failed to spawn the node process");
(node, args, temp_dir)
}
/// A shared node that multiple tests can use. It starts up once.
fn shared_node() -> &'static KitchensinkNode {
static NODE: LazyLock<(KitchensinkNode, TempDir)> = LazyLock::new(|| {
let (node, _, temp_dir) = new_node();
(node, temp_dir)
});
&NODE.0
}
#[tokio::test] #[tokio::test]
async fn node_mines_simple_transfer_transaction_and_returns_receipt() { async fn node_mines_simple_transfer_transaction_and_returns_receipt() {
// Arrange // Arrange
let (args, _temp_dir) = test_config(); let (node, args, _temp_dir) = new_node();
let mut node = KitchensinkNode::new(&args);
node.spawn(GENESIS_JSON.to_owned())
.expect("Failed to spawn the node");
let provider = ProviderBuilder::new() let provider = node.provider().await.expect("Failed to create provider");
.network::<KitchenSinkNetwork>()
.wallet(args.wallet())
.connect(&node.rpc_url)
.await
.expect("Failed to create provider");
let account_address = args.wallet().default_signer().address(); let account_address = args.wallet().default_signer().address();
let transaction = TransactionRequest::default() let transaction = TransactionRequest::default()
@@ -1137,4 +1261,92 @@ mod tests {
"Expected eth-rpc version string, got: {version}" "Expected eth-rpc version string, got: {version}"
); );
} }
#[test]
fn can_get_chain_id_from_node() {
// Arrange
let node = shared_node();
// Act
let chain_id = node.chain_id();
// Assert
let chain_id = chain_id.expect("Failed to get the chain id");
assert_eq!(chain_id, 420_420_420);
}
#[test]
fn can_get_gas_limit_from_node() {
// Arrange
let node = shared_node();
// Act
let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest);
// Assert
let _ = gas_limit.expect("Failed to get the gas limit");
}
#[test]
fn can_get_coinbase_from_node() {
// Arrange
let node = shared_node();
// Act
let coinbase = node.block_coinbase(BlockNumberOrTag::Latest);
// Assert
let coinbase = coinbase.expect("Failed to get the coinbase");
assert_eq!(coinbase, Address::ZERO)
}
#[test]
fn can_get_block_difficulty_from_node() {
// Arrange
let node = shared_node();
// Act
let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest);
// Assert
let block_difficulty = block_difficulty.expect("Failed to get the block difficulty");
assert_eq!(block_difficulty, U256::ZERO)
}
#[test]
fn can_get_block_hash_from_node() {
// Arrange
let node = shared_node();
// Act
let block_hash = node.block_hash(BlockNumberOrTag::Latest);
// Assert
let _ = block_hash.expect("Failed to get the block hash");
}
#[test]
fn can_get_block_timestamp_from_node() {
// Arrange
let node = shared_node();
// Act
let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest);
// Assert
let _ = block_timestamp.expect("Failed to get the block timestamp");
}
#[test]
fn can_get_block_number_from_node() {
// Arrange
let node = shared_node();
// Act
let block_number = node.last_block_number();
// Assert
let block_number = block_number.expect("Failed to get the block number");
assert_eq!(block_number, 0)
}
} }