Merge branch 'feature/better-input-parser' into omar-temp

This commit is contained in:
Omar Abdulla
2025-07-16 15:34:28 +03:00
9 changed files with 760 additions and 109 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",
+12
View File
@@ -78,3 +78,15 @@ features = [
inherits = "release" inherits = "release"
lto = true lto = true
codegen-units = 1 codegen-units = 1
# We set the `panic` behavior to `unwind` in both the `release` and `dev` profiles since our async
# runtime attempts to catch panics and it can only catch panics if we compile our code with unwind
# as the panic behavior. For more information, please see the `catch_unwind` documentation where it
# mentions that panics can only be caught if they unwind and not if they abort:
# https://doc.rust-lang.org/std/panic/fn.catch_unwind.html#notes
[profile.release]
panic = "unwind"
[profile.dev]
panic = "unwind"
+22 -16
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::{
@@ -134,17 +134,21 @@ 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,
Ok(tx) => { &self.deployed_contracts,
tracing::debug!("Legacy transaction data: {tx:#?}"); &self.deployed_abis,
tx node,
} ) {
Err(err) => { Ok(tx) => {
tracing::error!("Failed to construct legacy transaction: {err:?}"); tracing::debug!("Legacy transaction data: {tx:#?}");
return Err(err); tx
} }
}; Err(err) => {
tracing::error!("Failed to construct legacy transaction: {err:?}");
return Err(err);
}
};
tracing::trace!("Executing transaction for input: {input:?}"); tracing::trace!("Executing transaction for input: {input:?}");
@@ -233,10 +237,12 @@ where
// automatically fill in all of the missing fields from the provider that we // automatically fill in all of the missing fields from the provider that we
// are using. // are using.
let code = alloy::hex::decode(&code)?; let code = alloy::hex::decode(&code)?;
let tx = TransactionRequest::default() let tx = {
.nonce(nonce) let tx = TransactionRequest::default()
.from(input.caller) .nonce(nonce)
.with_deploy_code(code); .from(input.caller);
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,
+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 }
+233 -27
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,30 +224,40 @@ 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");
} else { let block_hash = chain_state_provider.block_hash(desired_block_number.into())?;
Ok(U256::from_str_radix(value, 10)
.map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?) 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))?)
} }
} }
@@ -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)
}
} }