diff --git a/Cargo.lock b/Cargo.lock index b7cfb19e..a8c4f41c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3984,6 +3984,7 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "anyhow", + "revive-dt-node-interaction", "semver 1.0.26", "serde", "serde_json", diff --git a/crates/core/src/driver/mod.rs b/crates/core/src/driver/mod.rs index f4d6acca..bed86427 100644 --- a/crates/core/src/driver/mod.rs +++ b/crates/core/src/driver/mod.rs @@ -1,7 +1,7 @@ //! The test driver handles the compilation and execution of the test cases. use alloy::json_abi::JsonAbi; -use alloy::network::TransactionBuilder; +use alloy::network::{Ethereum, TransactionBuilder}; use alloy::rpc::types::TransactionReceipt; use alloy::rpc::types::trace::geth::GethTrace; use alloy::{ @@ -134,17 +134,21 @@ where std::any::type_name::() ); - let tx = - match input.legacy_transaction(nonce, &self.deployed_contracts, &self.deployed_abis) { - Ok(tx) => { - tracing::debug!("Legacy transaction data: {tx:#?}"); - tx - } - Err(err) => { - tracing::error!("Failed to construct legacy transaction: {err:?}"); - return Err(err); - } - }; + let tx = match input.legacy_transaction( + nonce, + &self.deployed_contracts, + &self.deployed_abis, + node, + ) { + Ok(tx) => { + tracing::debug!("Legacy transaction data: {tx:#?}"); + tx + } + Err(err) => { + tracing::error!("Failed to construct legacy transaction: {err:?}"); + return Err(err); + } + }; tracing::trace!("Executing transaction for input: {input:?}"); @@ -253,10 +257,12 @@ where return Err(error.into()); } }; - let tx = TransactionRequest::default() - .nonce(nonce) - .from(input.caller) - .with_deploy_code(code); + let tx = { + let tx = TransactionRequest::default() + .nonce(nonce) + .from(input.caller); + TransactionBuilder::::with_deploy_code(tx, code) + }; let receipt = match node.execute_transaction(tx) { Ok(receipt) => receipt, diff --git a/crates/format/Cargo.toml b/crates/format/Cargo.toml index 4352683b..c1b7674a 100644 --- a/crates/format/Cargo.toml +++ b/crates/format/Cargo.toml @@ -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 } diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index 9275dca1..1cdefe41 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -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; + #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] pub struct Input { #[serde(default = "default_caller")] @@ -84,6 +87,7 @@ impl Input { &self, deployed_abis: &HashMap, deployed_contracts: &HashMap, + chain_state_provider: &impl EthereumNode, ) -> anyhow::Result { let Method::FunctionName(ref function_name) = self.method else { return Ok(Bytes::default()); // fallback or deployer — no input @@ -145,7 +149,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>()); } @@ -165,8 +169,10 @@ impl Input { nonce: u64, deployed_contracts: &HashMap, deployed_abis: &HashMap, + chain_state_provider: &impl EthereumNode, ) -> anyhow::Result { - 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); match self.method { Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)), @@ -196,6 +202,7 @@ fn default_caller() -> Address { fn resolve_argument( value: &str, deployed_contracts: &HashMap, + chain_state_provider: &impl EthereumNode, ) -> anyhow::Result { if let Some(instance) = value.strip_suffix(".address") { Ok(U256::from_be_slice( @@ -217,30 +224,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 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(); + + 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 { - // 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)); - - 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))?) - } + Ok(U256::from_str_radix(value, 10) + .map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?) } } @@ -253,6 +270,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 { + unimplemented!() + } + + fn trace_transaction( + &self, + _: alloy::rpc::types::TransactionReceipt, + ) -> anyhow::Result { + unimplemented!() + } + + fn state_diff( + &self, + _: alloy::rpc::types::TransactionReceipt, + ) -> anyhow::Result { + unimplemented!() + } + + fn fetch_add_nonce(&self, _: Address) -> anyhow::Result { + unimplemented!() + } + + fn chain_id(&self) -> anyhow::Result { + Ok(0x123) + } + + fn block_gas_limit(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result { + Ok(0x1234) + } + + fn block_coinbase(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result
{ + Ok(Address::ZERO) + } + + fn block_difficulty(&self, _: alloy::eips::BlockNumberOrTag) -> anyhow::Result { + Ok(U256::from(0x12345u128)) + } + + fn block_hash( + &self, + _: alloy::eips::BlockNumberOrTag, + ) -> anyhow::Result { + Ok([0xEE; 32].into()) + } + + fn block_timestamp( + &self, + _: alloy::eips::BlockNumberOrTag, + ) -> anyhow::Result { + Ok(0x123456) + } + + fn last_block_number(&self) -> anyhow::Result { + Ok(0x1234567) + } + } + #[test] fn test_encoded_input_uint256() { let raw_metadata = r#" @@ -288,7 +368,7 @@ mod tests { let deployed_contracts = HashMap::new(); let encoded = input - .encoded_input(&deployed_abis, &deployed_contracts) + .encoded_input(&deployed_abis, &deployed_contracts, &DummyEthereumNode) .unwrap(); assert!(encoded.0.starts_with(&selector)); @@ -331,7 +411,9 @@ mod tests { abis.insert("Contract".to_string(), parsed_abi); 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)); type T = (alloy_primitives::Address,); @@ -341,4 +423,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() + ) + ) + } } diff --git a/crates/node-interaction/src/blocking_executor.rs b/crates/node-interaction/src/blocking_executor.rs index 043dd197..baba0c17 100644 --- a/crates/node-interaction/src/blocking_executor.rs +++ b/crates/node-interaction/src/blocking_executor.rs @@ -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. diff --git a/crates/node-interaction/src/lib.rs b/crates/node-interaction/src/lib.rs index 2c2eef54..afba76a9 100644 --- a/crates/node-interaction/src/lib.rs +++ b/crates/node-interaction/src/lib.rs @@ -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; + fn execute_transaction(&self, transaction: TransactionRequest) -> Result; /// Trace the transaction in the [TransactionReceipt] and return a [GethTrace]. - fn trace_transaction(&self, transaction: TransactionReceipt) -> anyhow::Result; + fn trace_transaction(&self, transaction: TransactionReceipt) -> Result; /// Returns the state diff of the transaction hash in the [TransactionReceipt]. - fn state_diff(&self, transaction: TransactionReceipt) -> anyhow::Result; + fn state_diff(&self, transaction: TransactionReceipt) -> Result; /// Returns the next available nonce for the given [Address]. - fn fetch_add_nonce(&self, address: Address) -> anyhow::Result; + fn fetch_add_nonce(&self, address: Address) -> Result; + + /// Returns the ID of the chain that the node is on. + fn chain_id(&self) -> Result; + + // 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; + + /// Returns the coinbase of the specified block. + fn block_coinbase(&self, number: BlockNumberOrTag) -> Result
; + + /// Returns the difficulty of the specified block. + fn block_difficulty(&self, number: BlockNumberOrTag) -> Result; + + /// Returns the hash of the specified block. + fn block_hash(&self, number: BlockNumberOrTag) -> Result; + + /// Returns the timestamp of the specified block, + fn block_timestamp(&self, number: BlockNumberOrTag) -> Result; + + /// Returns the number of the last block. + fn last_block_number(&self) -> Result; } diff --git a/crates/node/src/geth.rs b/crates/node/src/geth.rs index 57cb2dba..4efd626e 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -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 Provider, 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 { - 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 { - 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 { - let connection_string = self.connection_string.clone(); - let wallet = self.wallet.clone(); - + let provider = self.provider(); let onchain_nonce = BlockingExecutor::execute::>(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 { + 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 { + 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
{ + 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 { + 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 { + 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 { + 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 { + 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) + } } diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index fbb36d47..995b8ca2 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -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, + impl Provider, + KitchenSinkNetwork, + >, + >, + > + 'static { + let connection_string = self.connection_string(); + let wallet = self.wallet.clone(); + Box::pin(async move { + ProviderBuilder::new() + .network::() + .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 { - 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::() - .wallet(wallet) - .connect(&url) + Ok(provider .await? .send_transaction(transaction) .await? @@ -364,20 +386,15 @@ impl EthereumNode for KitchensinkNode { &self, transaction: TransactionReceipt, ) -> anyhow::Result { - 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::() - .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 { - let url = self.rpc_url.clone(); - let wallet = self.wallet.clone(); - + let provider = self.provider(); let onchain_nonce = BlockingExecutor::execute::>(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 { + 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 { + 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
{ + 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 { + 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 { + 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 { + 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 { + 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::() - .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,92 @@ 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 = 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) + } }