Merge remote-tracking branch 'origin/main' into refactor/contract-deployment-and-input-handling

This commit is contained in:
Omar Abdulla
2025-07-18 15:18:27 +03:00
9 changed files with 1117 additions and 161 deletions
Generated
+1
View File
@@ -3985,6 +3985,7 @@ dependencies = [
"alloy-primitives",
"alloy-sol-types",
"anyhow",
"revive-dt-node-interaction",
"semver 1.0.26",
"serde",
"serde_json",
+377 -53
View File
@@ -26,7 +26,7 @@ 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 tracing::Level;
use std::fmt::Debug;
use crate::Platform;
use crate::common::*;
@@ -163,11 +163,6 @@ where
);
let _guard = span.enter();
// The ordering of the following statements and the use of the IndexMap is very intentional
// here. The order in which we do the deployments matters. For example, say that this is
// a `#deployer` call and the first argument is `Callable.address` which has not yet been
// deployed. This means that we need to deploy it first and then deploy then deploy the one
// from the input.
let mut instances_we_must_deploy = IndexMap::<ContractInstance, bool>::new();
for instance in input.find_all_contract_instances().into_iter() {
if !self
@@ -241,8 +236,8 @@ where
};
if deploy_with_constructor_arguments {
let encoded_input =
input.encoded_input(self.deployed_contracts.entry(case_idx).or_default())?;
let encoded_input = input
.encoded_input(self.deployed_contracts.entry(case_idx).or_default(), node)?;
code.extend(encoded_input.to_vec());
}
@@ -327,7 +322,7 @@ where
.clone(),
Method::Fallback | Method::FunctionName(_) => {
let tx = match input
.legacy_transaction(self.deployed_contracts.entry(case_idx).or_default())
.legacy_transaction(self.deployed_contracts.entry(case_idx).or_default(), node)
{
Ok(tx) => {
tracing::debug!("Legacy transaction data: {tx:#?}");
@@ -426,58 +421,159 @@ 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() {
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);
leader_state.build_contracts(&mode, self.metadata)?;
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() {
// Creating a tracing span to know which case within the metadata is being executed
// and which one we're getting logs for.
let tracing_span = tracing::span!(
Level::INFO,
"Executing case",
case = case.name,
// We build the contracts. If building the contracts for the metadata file fails then we
// have no other option but to keep note of this error and move on to the next solc mode
// and NOT just bail out of the execution as a whole.
let build_result = tracing::info_span!("Building contracts").in_scope(|| {
match leader_state.build_contracts(&mode, self.metadata) {
Ok(_) => {
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
);
let _guard = tracing_span.enter();
let case_idx = CaseIdx::from(case_idx);
for input in &case.inputs {
tracing::debug!("Starting executing contract {}", &*input.instance);
let (leader_receipt, _, leader_diff) = match leader_state.handle_input(
self.metadata,
case_idx,
input,
self.leader_node,
) {
Ok(result) => result,
Err(err) => {
tracing::error!(
"Leader execution failed for {}: {err}",
*input.instance
);
continue;
}
};
let case_idx = CaseIdx::new_from(case_idx);
let (follower_receipt, _, follower_diff) = match follower_state.handle_input(
self.metadata,
case_idx,
input,
self.follower_node,
) {
Ok(result) => result,
Err(err) => {
tracing::error!(
"Follower execution failed for {}: {err}",
*input.instance
);
continue;
}
// For inputs if one of the inputs fail we move on to the next case (we do not move
// on to the next input as it doesn't make sense. It depends on the previous one).
for (input_idx, input) in case.inputs.iter().enumerate() {
let tracing_span = tracing::info_span!("Handling input", input_idx);
let _guard = tracing_span.enter();
let execution_result =
tracing::info_span!("Executing input", contract_name = ?input.instance)
.in_scope(|| {
let (leader_receipt, _, leader_diff) = match leader_state
.handle_input(self.metadata, case_idx, input, self.leader_node)
{
Ok(result) => result,
Err(error) => {
tracing::error!(
target = ?Target::Leader,
?error,
"Contract execution failed"
);
execution_result.add_failed_case(
Target::Leader,
mode.clone(),
case.name
.as_deref()
.unwrap_or("no case name")
.to_owned(),
case_idx,
input_idx,
anyhow::Error::msg(format!("{error}")),
);
return Err(error);
}
};
let (follower_receipt, _, follower_diff) = match follower_state
.handle_input(
self.metadata,
case_idx,
input,
self.follower_node,
) {
Ok(result) => result,
Err(error) => {
tracing::error!(
target = ?Target::Follower,
?error,
"Contract execution failed"
);
execution_result.add_failed_case(
Target::Follower,
mode.clone(),
case.name
.as_deref()
.unwrap_or("no case name")
.to_owned(),
case_idx,
input_idx,
anyhow::Error::msg(format!("{error}")),
);
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 {
@@ -502,9 +598,237 @@ 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: CaseIdx,
) {
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: CaseIdx,
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, &CaseIdx)>;
/// 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, &CaseIdx)> {
None
}
fn input_index(&self) -> Option<usize> {
None
}
}
#[derive(Debug)]
pub enum CaseResult {
Success {
target: Target,
solc_mode: SolcMode,
case_name: String,
case_idx: CaseIdx,
},
Failure {
target: Target,
solc_mode: SolcMode,
case_name: String,
case_idx: CaseIdx,
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, &CaseIdx)> {
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),
}
}
}
+19 -14
View File
@@ -13,7 +13,7 @@ use revive_dt_node::pool::NodePool;
use revive_dt_report::reporter::{Report, Span};
use temp_dir::TempDir;
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());
@@ -39,7 +39,7 @@ fn init_cli() -> anyhow::Result<Arguments> {
.with_thread_ids(true)
.with_thread_names(true)
.with_env_filter(EnvFilter::from_default_env())
.with_span_events(FmtSpan::ENTER | FmtSpan::CLOSE)
.with_ansi(false)
.pretty()
.finish();
tracing::subscriber::set_global_default(subscriber)?;
@@ -116,20 +116,25 @@ where
follower_nodes.round_robbin(),
);
match driver.execute(span) {
Ok(_) => {
tracing::info!(
"metadata {} success",
metadata.directory().as_ref().unwrap().display()
);
}
Err(error) => {
tracing::warn!(
"metadata {} failure: {error:?}",
metadata.file_path.as_ref().unwrap().display()
);
let execution_result = driver.execute(span);
tracing::info!(
case_success_count = execution_result.successful_cases_count,
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;
}
}
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
[dependencies]
revive-dt-node-interaction = { workspace = true }
alloy = { workspace = true }
alloy-primitives = { workspace = true }
alloy-sol-types = { workspace = true }
+231 -28
View File
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use alloy::{
eips::BlockNumberOrTag,
json_abi::JsonAbi,
network::TransactionBuilder,
primitives::{Address, Bytes, U256},
@@ -10,6 +11,8 @@ use semver::VersionReq;
use serde::Deserialize;
use serde_json::Value;
use revive_dt_node_interaction::EthereumNode;
use crate::metadata::ContractInstance;
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
@@ -105,6 +108,7 @@ impl Input {
pub fn encoded_input(
&self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl EthereumNode,
) -> anyhow::Result<Bytes> {
match self.method {
Method::Deployer | Method::Fallback => {
@@ -115,7 +119,7 @@ impl Input {
let mut calldata = Vec::<u8>::with_capacity(calldata_args.len() * 32);
for (arg_idx, arg) in calldata_args.iter().enumerate() {
match resolve_argument(arg, deployed_contracts) {
match resolve_argument(arg, deployed_contracts, chain_state_provider) {
Ok(resolved) => {
calldata.extend(resolved.to_be_bytes::<32>());
}
@@ -177,7 +181,7 @@ impl Input {
calldata.extend(function.selector().0);
for (arg_idx, arg) in calldata_args.iter().enumerate() {
match resolve_argument(arg, deployed_contracts) {
match resolve_argument(arg, deployed_contracts, chain_state_provider) {
Ok(resolved) => {
calldata.extend(resolved.to_be_bytes::<32>());
}
@@ -197,8 +201,9 @@ impl Input {
pub fn legacy_transaction(
&self,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl EthereumNode,
) -> anyhow::Result<TransactionRequest> {
let input_data = self.encoded_input(deployed_contracts)?;
let input_data = self.encoded_input(deployed_contracts, chain_state_provider)?;
let transaction_request = TransactionRequest::default();
match self.method {
Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)),
@@ -253,6 +258,7 @@ fn default_caller() -> Address {
fn resolve_argument(
value: &str,
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
chain_state_provider: &impl EthereumNode,
) -> anyhow::Result<U256> {
if let Some(instance) = value.strip_suffix(".address") {
Ok(U256::from_be_slice(
@@ -275,30 +281,40 @@ fn resolve_argument(
} else if let Some(value) = value.strip_prefix("0x") {
Ok(U256::from_str_radix(value, 16)
.map_err(|error| anyhow::anyhow!("Invalid hexadecimal literal: {}", error))?)
} else {
// TODO: This is a set of "variables" that we need to be able to resolve to be fully in
// compliance with the matter labs tester but we currently do not resolve them. We need to
// add logic that does their resolution in the future, perhaps through some kind of system
// context API that we pass down to the resolution function that allows it to make calls to
// the node to perform these resolutions.
let is_unsupported = [
"$CHAIN_ID",
"$GAS_LIMIT",
"$COINBASE",
"$DIFFICULTY",
"$BLOCK_HASH",
"$BLOCK_TIMESTAMP",
]
.iter()
.any(|var| value.starts_with(var));
} else if value == "$CHAIN_ID" {
let chain_id = chain_state_provider.chain_id()?;
Ok(U256::from(chain_id))
} else if value == "$GAS_LIMIT" {
let gas_limit = chain_state_provider.block_gas_limit(BlockNumberOrTag::Latest)?;
Ok(U256::from(gas_limit))
} else if value == "$COINBASE" {
let coinbase = chain_state_provider.block_coinbase(BlockNumberOrTag::Latest)?;
Ok(U256::from_be_slice(coinbase.as_ref()))
} else if value == "$DIFFICULTY" {
let block_difficulty = chain_state_provider.block_difficulty(BlockNumberOrTag::Latest)?;
Ok(block_difficulty)
} else if value.starts_with("$BLOCK_HASH") {
let offset: u64 = value
.split(':')
.next_back()
.and_then(|value| value.parse().ok())
.unwrap_or_default();
if is_unsupported {
tracing::error!(value, "Unsupported variable used");
anyhow::bail!("Encountered {value} which is currently unsupported by the framework");
} else {
Ok(U256::from_str_radix(value, 10)
.map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?)
}
let current_block_number = chain_state_provider.last_block_number()?;
let desired_block_number = current_block_number - offset;
let block_hash = chain_state_provider.block_hash(desired_block_number.into())?;
Ok(U256::from_be_bytes(block_hash.0))
} else if value == "$BLOCK_NUMBER" {
let current_block_number = chain_state_provider.last_block_number()?;
Ok(U256::from(current_block_number))
} else if value == "$BLOCK_TIMESTAMP" {
let timestamp = chain_state_provider.block_timestamp(BlockNumberOrTag::Latest)?;
Ok(U256::from(timestamp))
} else {
Ok(U256::from_str_radix(value, 10)
.map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?)
}
}
@@ -311,6 +327,69 @@ mod tests {
use alloy_sol_types::SolValue;
use std::collections::HashMap;
struct DummyEthereumNode;
impl EthereumNode for DummyEthereumNode {
fn execute_transaction(
&self,
_: TransactionRequest,
) -> anyhow::Result<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]
fn test_encoded_input_uint256() {
let raw_metadata = r#"
@@ -347,7 +426,7 @@ mod tests {
(Address::ZERO, parsed_abi),
);
let encoded = input.encoded_input(&contracts).unwrap();
let encoded = input.encoded_input(&contracts, &DummyEthereumNode).unwrap();
assert!(encoded.0.starts_with(&selector));
type T = (u64,);
@@ -391,7 +470,7 @@ mod tests {
(Address::ZERO, parsed_abi),
);
let encoded = input.encoded_input(&contracts).unwrap();
let encoded = input.encoded_input(&contracts, &DummyEthereumNode).unwrap();
assert!(encoded.0.starts_with(&selector));
type T = (alloy_primitives::Address,);
@@ -401,4 +480,128 @@ mod tests {
address!("0x1000000000000000000000000000000000000001")
);
}
#[test]
fn resolver_can_resolve_chain_id_variable() {
// Arrange
let input = "$CHAIN_ID";
// Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(resolved, U256::from(DummyEthereumNode.chain_id().unwrap()))
}
#[test]
fn resolver_can_resolve_gas_limit_variable() {
// Arrange
let input = "$GAS_LIMIT";
// Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from(
DummyEthereumNode
.block_gas_limit(Default::default())
.unwrap()
)
)
}
#[test]
fn resolver_can_resolve_coinbase_variable() {
// Arrange
let input = "$COINBASE";
// Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from_be_slice(
DummyEthereumNode
.block_coinbase(Default::default())
.unwrap()
.as_ref()
)
)
}
#[test]
fn resolver_can_resolve_block_difficulty_variable() {
// Arrange
let input = "$DIFFICULTY";
// Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
DummyEthereumNode
.block_difficulty(Default::default())
.unwrap()
)
}
#[test]
fn resolver_can_resolve_block_hash_variable() {
// Arrange
let input = "$BLOCK_HASH";
// Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from_be_bytes(DummyEthereumNode.block_hash(Default::default()).unwrap().0)
)
}
#[test]
fn resolver_can_resolve_block_number_variable() {
// Arrange
let input = "$BLOCK_NUMBER";
// Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from(DummyEthereumNode.last_block_number().unwrap())
)
}
#[test]
fn resolver_can_resolve_block_timestamp_variable() {
// Arrange
let input = "$BLOCK_TIMESTAMP";
// Act
let resolved = resolve_argument(input, &Default::default(), &DummyEthereumNode);
// Assert
let resolved = resolved.expect("Failed to resolve argument");
assert_eq!(
resolved,
U256::from(
DummyEthereumNode
.block_timestamp(Default::default())
.unwrap()
)
)
}
}
@@ -144,7 +144,6 @@ impl BlockingExecutor {
}
}
}
/// 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
/// runtime.
+30 -8
View File
@@ -1,8 +1,10 @@
//! 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::{TransactionReceipt, TransactionRequest};
use anyhow::Result;
mod blocking_executor;
pub use blocking_executor::*;
@@ -10,17 +12,37 @@ pub use blocking_executor::*;
/// An interface for all interactions with Ethereum compatible nodes.
pub trait EthereumNode {
/// Execute the [TransactionRequest] and return a [TransactionReceipt].
fn execute_transaction(
&self,
transaction: TransactionRequest,
) -> anyhow::Result<TransactionReceipt>;
fn execute_transaction(&self, transaction: TransactionRequest) -> Result<TransactionReceipt>;
/// 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].
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].
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::{
network::EthereumWallet,
primitives::Address,
providers::{Provider, ProviderBuilder, ext::DebugApi},
eips::BlockNumberOrTag,
network::{Ethereum, EthereumWallet},
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, U256},
providers::{
Provider, ProviderBuilder,
ext::DebugApi,
fillers::{FillProvider, TxFiller},
},
rpc::types::{
TransactionReceipt, TransactionRequest,
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
@@ -191,6 +196,24 @@ impl Instance {
fn geth_stderr_log_file_path(&self) -> PathBuf {
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 {
@@ -199,17 +222,12 @@ impl EthereumNode for Instance {
&self,
transaction: TransactionRequest,
) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> {
let connection_string = self.connection_string();
let wallet = self.wallet.clone();
let provider = self.provider();
BlockingExecutor::execute(async move {
let outer_span = tracing::debug_span!("Submitting transaction", ?transaction,);
let _outer_guard = outer_span.enter();
let provider = ProviderBuilder::new()
.wallet(wallet)
.connect(&connection_string)
.await?;
let provider = provider.await?;
let pending_transaction = provider.send_transaction(transaction).await?;
let transaction_hash = pending_transaction.tx_hash();
@@ -289,18 +307,15 @@ impl EthereumNode for Instance {
&self,
transaction: TransactionReceipt,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
let connection_string = self.connection_string();
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
let wallet = self.wallet.clone();
let provider = self.provider();
BlockingExecutor::execute(async move {
Ok(ProviderBuilder::new()
.wallet(wallet)
.connect(&connection_string)
Ok(provider
.await?
.debug_trace_transaction(transaction.transaction_hash, trace_options)
.await?)
@@ -323,13 +338,9 @@ impl EthereumNode for Instance {
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64> {
let connection_string = self.connection_string.clone();
let wallet = self.wallet.clone();
let provider = self.provider();
let onchain_nonce = BlockingExecutor::execute::<anyhow::Result<_>>(async move {
ProviderBuilder::new()
.wallet(wallet)
.connect(&connection_string)
provider
.await?
.get_transaction_count(address)
.await
@@ -342,6 +353,87 @@ impl EthereumNode for Instance {
*current += 1;
Ok(value)
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
let provider = self.provider();
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 {
@@ -429,7 +521,7 @@ mod tests {
use crate::{GENESIS_JSON, Node};
use super::Instance;
use super::*;
fn test_config() -> (Arguments, TempDir) {
let mut config = Arguments::default();
@@ -439,6 +531,16 @@ mod tests {
(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]
fn init_works() {
Instance::new(&test_config().0)
@@ -461,4 +563,93 @@ mod tests {
"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)
}
}
+244 -35
View File
@@ -13,13 +13,18 @@ use std::{
use alloy::{
consensus::{BlockHeader, TxEnvelope},
eips::BlockNumberOrTag,
hex,
network::{
Ethereum, EthereumWallet, Network, TransactionBuilder, TransactionBuilderError,
UnbuiltTransactionError,
},
primitives::{Address, B64, B256, BlockNumber, Bloom, Bytes, U256},
providers::{Provider, ProviderBuilder, ext::DebugApi},
primitives::{Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, U256},
providers::{
Provider, ProviderBuilder,
ext::DebugApi,
fillers::{FillProvider, TxFiller},
},
rpc::types::{
TransactionReceipt,
eth::{Block, Header, Transaction},
@@ -232,6 +237,7 @@ impl KitchensinkNode {
Ok(())
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn extract_balance_from_genesis_file(
&self,
@@ -330,6 +336,29 @@ impl KitchensinkNode {
fn proxy_stderr_log_file_path(&self) -> PathBuf {
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 {
@@ -338,17 +367,10 @@ impl EthereumNode for KitchensinkNode {
&self,
transaction: alloy::rpc::types::TransactionRequest,
) -> anyhow::Result<TransactionReceipt> {
let url = self.rpc_url.clone();
let wallet = self.wallet.clone();
tracing::debug!("Submitting transaction: {transaction:#?}");
tracing::info!("Submitting tx to kitchensink");
tracing::debug!(?transaction, "Submitting transaction");
let provider = self.provider();
let receipt = BlockingExecutor::execute(async move {
Ok(ProviderBuilder::new()
.network::<KitchenSinkNetwork>()
.wallet(wallet)
.connect(&url)
Ok(provider
.await?
.send_transaction(transaction)
.await?
@@ -364,20 +386,15 @@ impl EthereumNode for KitchensinkNode {
&self,
transaction: TransactionReceipt,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
let url = self.rpc_url.clone();
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
let wallet = self.wallet.clone();
let provider = self.provider();
BlockingExecutor::execute(async move {
Ok(ProviderBuilder::new()
.network::<KitchenSinkNetwork>()
.wallet(wallet)
.connect(&url)
Ok(provider
.await?
.debug_trace_transaction(transaction.transaction_hash, trace_options)
.await?)
@@ -397,13 +414,9 @@ impl EthereumNode for KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64> {
let url = self.rpc_url.clone();
let wallet = self.wallet.clone();
let provider = self.provider();
let onchain_nonce = BlockingExecutor::execute::<anyhow::Result<_>>(async move {
ProviderBuilder::new()
.wallet(wallet)
.connect(&url)
provider
.await?
.get_transaction_count(address)
.await
@@ -416,6 +429,87 @@ impl EthereumNode for KitchensinkNode {
*current += 1;
Ok(value)
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
let provider = self.provider();
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 {
@@ -926,6 +1020,7 @@ mod tests {
use alloy::rpc::types::TransactionRequest;
use revive_dt_config::Arguments;
use std::path::PathBuf;
use std::sync::LazyLock;
use temp_dir::TempDir;
use std::fs;
@@ -945,20 +1040,49 @@ mod tests {
(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]
async fn node_mines_simple_transfer_transaction_and_returns_receipt() {
// Arrange
let (args, _temp_dir) = test_config();
let mut node = KitchensinkNode::new(&args);
node.spawn(GENESIS_JSON.to_owned())
.expect("Failed to spawn the node");
let (node, args, _temp_dir) = new_node();
let provider = ProviderBuilder::new()
.network::<KitchenSinkNetwork>()
.wallet(args.wallet())
.connect(&node.rpc_url)
.await
.expect("Failed to create provider");
let provider = node.provider().await.expect("Failed to create provider");
let account_address = args.wallet().default_signer().address();
let transaction = TransactionRequest::default()
@@ -1137,4 +1261,89 @@ mod tests {
"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.expect("Failed to get the coinbase");
}
#[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.expect("Failed to get the block difficulty");
}
#[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.expect("Failed to get the block number");
}
}