From abba0cee08105c8721bcd493b163c888d2601c65 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Fri, 11 Jul 2025 11:26:08 +0300 Subject: [PATCH 01/25] Introduce a custom kitchensink network --- .gitignore | 4 + Cargo.lock | 1 + Cargo.toml | 3 + crates/node/Cargo.toml | 1 + crates/node/src/kitchensink.rs | 457 ++++++++++++++++++++++++++++++++- 5 files changed, 456 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 10a2f71e..cf26eca1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ .DS_Store node_modules /*.json + +# We do not want to commit any log files that we produce from running the code locally so this is +# added to the .gitignore file. +*.log \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9d729db5..0cff4c8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3998,6 +3998,7 @@ dependencies = [ "anyhow", "revive-dt-config", "revive-dt-node-interaction", + "serde", "serde_json", "sp-core", "sp-runtime", diff --git a/Cargo.toml b/Cargo.toml index a28e1947..5d1d6a7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,9 @@ features = [ "rpc-types", "signer-local", "std", + "network", + "serde", + "rpc-types-eth", ] [profile.bench] diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 1a9dcb0a..1b54fc95 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -16,6 +16,7 @@ tracing = { workspace = true } revive-dt-node-interaction = { workspace = true } revive-dt-config = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } sp-core = { workspace = true } diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index 4f14a18b..de6d216f 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -12,16 +12,15 @@ use std::{ }; use alloy::{ - hex, - network::EthereumWallet, - primitives::Address, - providers::{Provider, ProviderBuilder, ext::DebugApi}, - rpc::types::{ - TransactionReceipt, - trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, - }, + consensus::{BlockHeader, TxEnvelope}, hex, network::{ + Ethereum, EthereumWallet, Network, TransactionBuilder, TransactionBuilderError, + UnbuiltTransactionError, + }, primitives::{Address, BlockNumber, Bloom, Bytes, B256, B64, U256}, providers::{ext::DebugApi, Provider, ProviderBuilder}, rpc::types::{ + eth::{Block, Header, Transaction}, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, TransactionReceipt + } }; use serde_json::{Value as JsonValue, json}; +use serde::{Serialize, Deserialize}; use sp_core::crypto::Ss58Codec; use sp_runtime::AccountId32; @@ -254,8 +253,10 @@ impl EthereumNode for KitchensinkNode { tracing::debug!("Submitting transaction: {transaction:#?}"); - execute_transaction(Box::pin(async move { + tracing::info!("Submitting tx to kitchensink"); + let receipt = execute_transaction(Box::pin(async move { Ok(ProviderBuilder::new() + .network::() .wallet(wallet) .connect(&url) .await? @@ -263,7 +264,9 @@ impl EthereumNode for KitchensinkNode { .await? .get_receipt() .await?) - })) + })); + tracing::info!(?receipt, "Submitted tx to kitchensink"); + receipt } fn trace_transaction( @@ -281,6 +284,7 @@ impl EthereumNode for KitchensinkNode { trace_transaction(Box::pin(async move { Ok(ProviderBuilder::new() + .network::() .wallet(wallet) .connect(&url) .await? @@ -374,6 +378,439 @@ impl Drop for KitchensinkNode { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct KitchenSinkNetwork; + +impl Network for KitchenSinkNetwork { + type TxType = ::TxType; + + type TxEnvelope = ::TxEnvelope; + + type UnsignedTx = ::UnsignedTx; + + type ReceiptEnvelope = ::ReceiptEnvelope; + + type Header = KitchenSinkHeader; + + type TransactionRequest = ::TransactionRequest; + + type TransactionResponse = ::TransactionResponse; + + type ReceiptResponse = ::ReceiptResponse; + + type HeaderResponse = Header; + + type BlockResponse = Block, Header>; +} + +impl TransactionBuilder for ::TransactionRequest { + fn chain_id(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::chain_id(self) + } + + fn set_chain_id(&mut self, chain_id: alloy::primitives::ChainId) { + <::TransactionRequest as TransactionBuilder>::set_chain_id( + self, chain_id, + ) + } + + fn nonce(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::nonce(self) + } + + fn set_nonce(&mut self, nonce: u64) { + <::TransactionRequest as TransactionBuilder>::set_nonce( + self, nonce, + ) + } + + fn input(&self) -> Option<&alloy::primitives::Bytes> { + <::TransactionRequest as TransactionBuilder>::input(self) + } + + fn set_input>(&mut self, input: T) { + <::TransactionRequest as TransactionBuilder>::set_input( + self, input, + ) + } + + fn from(&self) -> Option
{ + <::TransactionRequest as TransactionBuilder>::from( + self, + ) + } + + fn set_from(&mut self, from: Address) { + <::TransactionRequest as TransactionBuilder>::set_from( + self, from + ) + } + + fn kind(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::kind( + self, + ) + } + + fn clear_kind(&mut self) { + <::TransactionRequest as TransactionBuilder>::clear_kind( + self, + ) + } + + fn set_kind(&mut self, kind: alloy::primitives::TxKind) { + <::TransactionRequest as TransactionBuilder>::set_kind( + self, kind + ) + } + + fn value(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::value( + self + ) + } + + fn set_value(&mut self, value: alloy::primitives::U256) { + <::TransactionRequest as TransactionBuilder>::set_value( + self, value + ) + } + + fn gas_price(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::gas_price( + self, + ) + } + + fn set_gas_price(&mut self, gas_price: u128) { + <::TransactionRequest as TransactionBuilder>::set_gas_price( + self, gas_price + ) + } + + fn max_fee_per_gas(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::max_fee_per_gas( + self, + ) + } + + fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) { + <::TransactionRequest as TransactionBuilder>::set_max_fee_per_gas( + self, max_fee_per_gas + ) + } + + fn max_priority_fee_per_gas(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::max_priority_fee_per_gas( + self, + ) + } + + fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) { + <::TransactionRequest as TransactionBuilder>::set_max_priority_fee_per_gas( + self, max_priority_fee_per_gas + ) + } + + fn gas_limit(&self) -> Option { + <::TransactionRequest as TransactionBuilder>::gas_limit( + self, + ) + } + + fn set_gas_limit(&mut self, gas_limit: u64) { + <::TransactionRequest as TransactionBuilder>::set_gas_limit( + self, gas_limit + ) + } + + fn access_list(&self) -> Option<&alloy::rpc::types::AccessList> { + <::TransactionRequest as TransactionBuilder>::access_list( + self, + ) + } + + fn set_access_list(&mut self, access_list: alloy::rpc::types::AccessList) { + <::TransactionRequest as TransactionBuilder>::set_access_list( + self, access_list + ) + } + + fn complete_type( + &self, + ty: ::TxType, + ) -> Result<(), Vec<&'static str>> { + <::TransactionRequest as TransactionBuilder>::complete_type( + self, ty + ) + } + + fn can_submit(&self) -> bool { + <::TransactionRequest as TransactionBuilder>::can_submit( + self, + ) + } + + fn can_build(&self) -> bool { + <::TransactionRequest as TransactionBuilder>::can_build( + self, + ) + } + + fn output_tx_type(&self) -> ::TxType { + <::TransactionRequest as TransactionBuilder>::output_tx_type( + self, + ) + } + + fn output_tx_type_checked(&self) -> Option<::TxType> { + <::TransactionRequest as TransactionBuilder>::output_tx_type_checked( + self, + ) + } + + fn prep_for_submission(&mut self) { + <::TransactionRequest as TransactionBuilder>::prep_for_submission( + self, + ) + } + + fn build_unsigned( + self, + ) -> alloy::network::BuildResult<::UnsignedTx, KitchenSinkNetwork> + { + let result = <::TransactionRequest as TransactionBuilder>::build_unsigned( + self, + ); + match result { + Ok(unsigned_tx) => Ok(unsigned_tx), + Err(UnbuiltTransactionError { request, error }) => { + Err(UnbuiltTransactionError:: { + request: request, + error: match error { + TransactionBuilderError::InvalidTransactionRequest(tx_type, items) => { + TransactionBuilderError::InvalidTransactionRequest(tx_type, items) + } + TransactionBuilderError::UnsupportedSignatureType => { + TransactionBuilderError::UnsupportedSignatureType + } + TransactionBuilderError::Signer(error) => { + TransactionBuilderError::Signer(error) + } + TransactionBuilderError::Custom(error) => { + TransactionBuilderError::Custom(error) + } + }, + }) + } + } + } + + async fn build>( + self, + wallet: &W, + ) -> Result< + ::TxEnvelope, + TransactionBuilderError, + > { + Ok(wallet.sign_request(self).await?) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct KitchenSinkHeader { + /// The Keccak 256-bit hash of the parent + /// block’s header, in its entirety; formally Hp. + pub parent_hash: B256, + /// The Keccak 256-bit hash of the ommers list portion of this block; formally Ho. + #[serde(rename = "sha3Uncles", alias = "ommersHash")] + pub ommers_hash: B256, + /// The 160-bit address to which all fees collected from the successful mining of this block + /// be transferred; formally Hc. + #[serde(rename = "miner", alias = "beneficiary")] + pub beneficiary: Address, + /// The Keccak 256-bit hash of the root node of the state trie, after all transactions are + /// executed and finalisations applied; formally Hr. + pub state_root: B256, + /// The Keccak 256-bit hash of the root node of the trie structure populated with each + /// transaction in the transactions list portion of the block; formally Ht. + pub transactions_root: B256, + /// The Keccak 256-bit hash of the root node of the trie structure populated with the receipts + /// of each transaction in the transactions list portion of the block; formally He. + pub receipts_root: B256, + /// The Bloom filter composed from indexable information (logger address and log topics) + /// contained in each log entry from the receipt of each transaction in the transactions list; + /// formally Hb. + pub logs_bloom: Bloom, + /// A scalar value corresponding to the difficulty level of this block. This can be calculated + /// from the previous block’s difficulty level and the timestamp; formally Hd. + pub difficulty: U256, + /// A scalar value equal to the number of ancestor blocks. The genesis block has a number of + /// zero; formally Hi. + #[serde(with = "alloy::serde::quantity")] + pub number: BlockNumber, + /// A scalar value equal to the current limit of gas expenditure per block; formally Hl. + // This is the main difference over the Ethereum network implementation. We use u128 here and + // not u64. + #[serde(with = "alloy::serde::quantity")] + pub gas_limit: u128, + /// A scalar value equal to the total gas used in transactions in this block; formally Hg. + #[serde(with = "alloy::serde::quantity")] + pub gas_used: u64, + /// A scalar value equal to the reasonable output of Unix’s time() at this block’s inception; + /// formally Hs. + #[serde(with = "alloy::serde::quantity")] + pub timestamp: u64, + /// An arbitrary byte array containing data relevant to this block. This must be 32 bytes or + /// fewer; formally Hx. + pub extra_data: Bytes, + /// A 256-bit hash which, combined with the + /// nonce, proves that a sufficient amount of computation has been carried out on this block; + /// formally Hm. + pub mix_hash: B256, + /// A 64-bit value which, combined with the mixhash, proves that a sufficient amount of + /// computation has been carried out on this block; formally Hn. + pub nonce: B64, + /// A scalar representing EIP1559 base fee which can move up or down each block according + /// to a formula which is a function of gas used in parent block and gas target + /// (block gas limit divided by elasticity multiplier) of parent block. + /// The algorithm results in the base fee per gas increasing when blocks are + /// above the gas target, and decreasing when blocks are below the gas target. The base fee per + /// gas is burned. + #[serde( + default, + with = "alloy::serde::quantity::opt", + skip_serializing_if = "Option::is_none" + )] + pub base_fee_per_gas: Option, + /// The Keccak 256-bit hash of the withdrawals list portion of this block. + /// + #[serde(default, skip_serializing_if = "Option::is_none")] + pub withdrawals_root: Option, + /// The total amount of blob gas consumed by the transactions within the block, added in + /// EIP-4844. + #[serde( + default, + with = "alloy::serde::quantity::opt", + skip_serializing_if = "Option::is_none" + )] + pub blob_gas_used: Option, + /// A running total of blob gas consumed in excess of the target, prior to the block. Blocks + /// with above-target blob gas consumption increase this value, blocks with below-target blob + /// gas consumption decrease it (bounded at 0). This was added in EIP-4844. + #[serde( + default, + with = "alloy::serde::quantity::opt", + skip_serializing_if = "Option::is_none" + )] + pub excess_blob_gas: Option, + /// The hash of the parent beacon block's root is included in execution blocks, as proposed by + /// EIP-4788. + /// + /// This enables trust-minimized access to consensus state, supporting staking pools, bridges, + /// and more. + /// + /// The beacon roots contract handles root storage, enhancing Ethereum's functionalities. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parent_beacon_block_root: Option, + /// The Keccak 256-bit hash of the an RLP encoded list with each + /// [EIP-7685] request in the block body. + /// + /// [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 + #[serde(default, skip_serializing_if = "Option::is_none")] + pub requests_hash: Option, +} + +impl BlockHeader for KitchenSinkHeader { + fn parent_hash(&self) -> B256 { + self.parent_hash + } + + fn ommers_hash(&self) -> B256 { + self.ommers_hash + } + + fn beneficiary(&self) -> Address { + self.beneficiary + } + + fn state_root(&self) -> B256 { + self.state_root + } + + fn transactions_root(&self) -> B256 { + self.transactions_root + } + + fn receipts_root(&self) -> B256 { + self.receipts_root + } + + fn withdrawals_root(&self) -> Option { + self.withdrawals_root + } + + fn logs_bloom(&self) -> Bloom { + self.logs_bloom + } + + fn difficulty(&self) -> U256 { + self.difficulty + } + + fn number(&self) -> BlockNumber { + self.number + } + + // There's sadly nothing that we can do about this. We're required to implement this trait on + // any type that represents a header and the gas limit type used here is a u64. + fn gas_limit(&self) -> u64 { + self.gas_limit.try_into().unwrap_or(u64::MAX) + } + + fn gas_used(&self) -> u64 { + self.gas_used + } + + fn timestamp(&self) -> u64 { + self.timestamp + } + + fn mix_hash(&self) -> Option { + Some(self.mix_hash) + } + + fn nonce(&self) -> Option { + Some(self.nonce) + } + + fn base_fee_per_gas(&self) -> Option { + self.base_fee_per_gas + } + + fn blob_gas_used(&self) -> Option { + self.blob_gas_used + } + + fn excess_blob_gas(&self) -> Option { + self.excess_blob_gas + } + + fn parent_beacon_block_root(&self) -> Option { + self.parent_beacon_block_root + } + + fn requests_hash(&self) -> Option { + self.requests_hash + } + + fn extra_data(&self) -> &Bytes { + &self.extra_data + } +} + #[cfg(test)] mod tests { use revive_dt_config::Arguments; From f6374ad52aeab8c012b303a27842d6b9460911eb Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Fri, 11 Jul 2025 14:49:05 +0300 Subject: [PATCH 02/25] fix formatting --- crates/node/src/kitchensink.rs | 79 ++++++++++++++++------------------ 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index de6d216f..a36a6d69 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -12,15 +12,22 @@ use std::{ }; use alloy::{ - consensus::{BlockHeader, TxEnvelope}, hex, network::{ + consensus::{BlockHeader, TxEnvelope}, + hex, + network::{ Ethereum, EthereumWallet, Network, TransactionBuilder, TransactionBuilderError, UnbuiltTransactionError, - }, primitives::{Address, BlockNumber, Bloom, Bytes, B256, B64, U256}, providers::{ext::DebugApi, Provider, ProviderBuilder}, rpc::types::{ - eth::{Block, Header, Transaction}, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, TransactionReceipt - } + }, + primitives::{Address, B64, B256, BlockNumber, Bloom, Bytes, U256}, + providers::{Provider, ProviderBuilder, ext::DebugApi}, + rpc::types::{ + TransactionReceipt, + eth::{Block, Header, Transaction}, + trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, + }, }; +use serde::{Deserialize, Serialize}; use serde_json::{Value as JsonValue, json}; -use serde::{Serialize, Deserialize}; use sp_core::crypto::Ss58Codec; use sp_runtime::AccountId32; @@ -435,21 +442,17 @@ impl TransactionBuilder for ::Transacti } fn from(&self) -> Option
{ - <::TransactionRequest as TransactionBuilder>::from( - self, - ) + <::TransactionRequest as TransactionBuilder>::from(self) } fn set_from(&mut self, from: Address) { <::TransactionRequest as TransactionBuilder>::set_from( - self, from + self, from, ) } fn kind(&self) -> Option { - <::TransactionRequest as TransactionBuilder>::kind( - self, - ) + <::TransactionRequest as TransactionBuilder>::kind(self) } fn clear_kind(&mut self) { @@ -460,37 +463,33 @@ impl TransactionBuilder for ::Transacti fn set_kind(&mut self, kind: alloy::primitives::TxKind) { <::TransactionRequest as TransactionBuilder>::set_kind( - self, kind + self, kind, ) } fn value(&self) -> Option { - <::TransactionRequest as TransactionBuilder>::value( - self - ) + <::TransactionRequest as TransactionBuilder>::value(self) } fn set_value(&mut self, value: alloy::primitives::U256) { <::TransactionRequest as TransactionBuilder>::set_value( - self, value + self, value, ) } fn gas_price(&self) -> Option { - <::TransactionRequest as TransactionBuilder>::gas_price( - self, - ) + <::TransactionRequest as TransactionBuilder>::gas_price(self) } fn set_gas_price(&mut self, gas_price: u128) { <::TransactionRequest as TransactionBuilder>::set_gas_price( - self, gas_price + self, gas_price, ) } fn max_fee_per_gas(&self) -> Option { <::TransactionRequest as TransactionBuilder>::max_fee_per_gas( - self, + self, ) } @@ -502,7 +501,7 @@ impl TransactionBuilder for ::Transacti fn max_priority_fee_per_gas(&self) -> Option { <::TransactionRequest as TransactionBuilder>::max_priority_fee_per_gas( - self, + self, ) } @@ -513,26 +512,25 @@ impl TransactionBuilder for ::Transacti } fn gas_limit(&self) -> Option { - <::TransactionRequest as TransactionBuilder>::gas_limit( - self, - ) + <::TransactionRequest as TransactionBuilder>::gas_limit(self) } fn set_gas_limit(&mut self, gas_limit: u64) { <::TransactionRequest as TransactionBuilder>::set_gas_limit( - self, gas_limit + self, gas_limit, ) } fn access_list(&self) -> Option<&alloy::rpc::types::AccessList> { <::TransactionRequest as TransactionBuilder>::access_list( - self, + self, ) } fn set_access_list(&mut self, access_list: alloy::rpc::types::AccessList) { <::TransactionRequest as TransactionBuilder>::set_access_list( - self, access_list + self, + access_list, ) } @@ -541,37 +539,35 @@ impl TransactionBuilder for ::Transacti ty: ::TxType, ) -> Result<(), Vec<&'static str>> { <::TransactionRequest as TransactionBuilder>::complete_type( - self, ty + self, ty, ) } fn can_submit(&self) -> bool { <::TransactionRequest as TransactionBuilder>::can_submit( - self, + self, ) } fn can_build(&self) -> bool { - <::TransactionRequest as TransactionBuilder>::can_build( - self, - ) + <::TransactionRequest as TransactionBuilder>::can_build(self) } fn output_tx_type(&self) -> ::TxType { <::TransactionRequest as TransactionBuilder>::output_tx_type( - self, + self, ) } fn output_tx_type_checked(&self) -> Option<::TxType> { <::TransactionRequest as TransactionBuilder>::output_tx_type_checked( - self, + self, ) } fn prep_for_submission(&mut self) { <::TransactionRequest as TransactionBuilder>::prep_for_submission( - self, + self, ) } @@ -617,8 +613,7 @@ impl TransactionBuilder for ::Transacti } } -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -#[derive(Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct KitchenSinkHeader { /// The Keccak 256-bit hash of the parent @@ -652,7 +647,7 @@ pub struct KitchenSinkHeader { #[serde(with = "alloy::serde::quantity")] pub number: BlockNumber, /// A scalar value equal to the current limit of gas expenditure per block; formally Hl. - // This is the main difference over the Ethereum network implementation. We use u128 here and + // This is the main difference over the Ethereum network implementation. We use u128 here and // not u64. #[serde(with = "alloy::serde::quantity")] pub gas_limit: u128, @@ -764,8 +759,8 @@ impl BlockHeader for KitchenSinkHeader { self.number } - // There's sadly nothing that we can do about this. We're required to implement this trait on - // any type that represents a header and the gas limit type used here is a u64. + // There's sadly nothing that we can do about this. We're required to implement this trait on + // any type that represents a header and the gas limit type used here is a u64. fn gas_limit(&self) -> u64 { self.gas_limit.try_into().unwrap_or(u64::MAX) } From 4bab45711425db25b217347c0b522f03798280b5 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Fri, 11 Jul 2025 17:18:42 +0300 Subject: [PATCH 03/25] Added `--dev` to `substrate-node` arguments. This commit adds the `--dev` argument to the `substrate-node` to allow the chain to keep advancing as time goes own. We have found that if this option is not added then the chain won't advance forward. --- Cargo.lock | 1 + crates/node/Cargo.toml | 1 + crates/node/src/kitchensink.rs | 35 +++++++++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 0cff4c8b..c125feeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4003,6 +4003,7 @@ dependencies = [ "sp-core", "sp-runtime", "temp-dir", + "tokio", "tracing", ] diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 1b54fc95..8fd6150a 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -24,3 +24,4 @@ sp-runtime = { workspace = true } [dev-dependencies] temp-dir = { workspace = true } +tokio = { workspace = true } diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index a36a6d69..f93d5394 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -128,6 +128,7 @@ impl KitchensinkNode { // Start Substrate node let mut substrate_process = Command::new(&self.substrate_binary) + .arg("--dev") .arg("--chain") .arg(chainspec_path) .arg("--base-path") @@ -808,13 +809,14 @@ impl BlockHeader for KitchenSinkHeader { #[cfg(test)] mod tests { + use alloy::rpc::types::TransactionRequest; use revive_dt_config::Arguments; use std::path::PathBuf; use temp_dir::TempDir; use std::fs; - use super::KitchensinkNode; + use super::*; use crate::{GENESIS_JSON, Node}; fn test_config() -> (Arguments, TempDir) { @@ -829,6 +831,37 @@ mod tests { (config, temp_dir) } + #[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 provider = ProviderBuilder::new() + .network::() + .wallet(args.wallet()) + .connect(&node.rpc_url) + .await + .expect("Failed to create provider"); + + let account_address = args.wallet().default_signer().address(); + let transaction = TransactionRequest::default() + .to(account_address) + .value(U256::from(100_000_000_000_000u128)); + + // Act + let receipt = provider.send_transaction(transaction).await; + + // Assert + let _ = receipt + .expect("Failed to send the transfer transaction") + .get_receipt() + .await + .expect("Failed to get the receipt for the transfer"); + } + #[test] fn test_init_generates_chainspec_with_balances() { let genesis_content = r#" From 7664e9735e44809eb8d604acc01c89db480647fd Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Fri, 11 Jul 2025 17:20:36 +0300 Subject: [PATCH 04/25] fix clippy warning --- crates/node/src/kitchensink.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index f93d5394..e621164e 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -583,7 +583,7 @@ impl TransactionBuilder for ::Transacti Ok(unsigned_tx) => Ok(unsigned_tx), Err(UnbuiltTransactionError { request, error }) => { Err(UnbuiltTransactionError:: { - request: request, + request, error: match error { TransactionBuilderError::InvalidTransactionRequest(tx_type, items) => { TransactionBuilderError::InvalidTransactionRequest(tx_type, items) From 76c85f191ca3bd9f0ba3999556b36fdfe801dcfb Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Fri, 11 Jul 2025 17:31:42 +0300 Subject: [PATCH 05/25] fix clippy warning --- crates/node/src/kitchensink.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index a36a6d69..239fe13c 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -582,7 +582,7 @@ impl TransactionBuilder for ::Transacti Ok(unsigned_tx) => Ok(unsigned_tx), Err(UnbuiltTransactionError { request, error }) => { Err(UnbuiltTransactionError:: { - request: request, + request, error: match error { TransactionBuilderError::InvalidTransactionRequest(tx_type, items) => { TransactionBuilderError::InvalidTransactionRequest(tx_type, items) From e3723e780a63b147eb763b86f021db4d9bf29c4c Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Sun, 13 Jul 2025 19:52:06 +0300 Subject: [PATCH 06/25] Fix function selector and argument encoding --- Cargo.lock | 153 ++++++++++++++++- Cargo.toml | 1 + crates/format/Cargo.toml | 1 + crates/format/src/input.rs | 344 +++++++++++++------------------------ 4 files changed, 273 insertions(+), 226 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d729db5..81a5f8f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1586,6 +1586,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "core-foundation" version = "0.9.4" @@ -1808,6 +1814,19 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.101", +] + [[package]] name = "derive_more" version = "1.0.0" @@ -2093,6 +2112,50 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ethabi" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" +dependencies = [ + "ethereum-types", + "hex", + "once_cell", + "regex", + "serde", + "serde_json", + "sha3", + "thiserror 1.0.69", + "uint 0.9.5", +] + +[[package]] +name = "ethbloom" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" +dependencies = [ + "crunchy", + "fixed-hash", + "impl-rlp", + "impl-serde 0.4.0", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-rlp", + "impl-serde 0.4.0", + "primitive-types 0.12.2", + "uint 0.9.5", +] + [[package]] name = "expander" version = "2.2.1" @@ -2297,6 +2360,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -2759,6 +2828,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "1.0.3" @@ -2809,6 +2888,24 @@ dependencies = [ "uint 0.10.0", ] +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde", +] + [[package]] name = "impl-serde" version = "0.5.0" @@ -2949,6 +3046,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonrpc-core" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" +dependencies = [ + "futures", + "futures-executor", + "futures-util", + "log", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "k256" version = "0.13.4" @@ -3592,6 +3704,8 @@ checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" dependencies = [ "fixed-hash", "impl-codec 0.6.0", + "impl-rlp", + "impl-serde 0.4.0", "uint 0.9.5", ] @@ -3604,7 +3718,7 @@ dependencies = [ "fixed-hash", "impl-codec 0.7.1", "impl-num-traits", - "impl-serde", + "impl-serde 0.5.0", "scale-info", "uint 0.10.0", ] @@ -3988,6 +4102,7 @@ dependencies = [ "serde", "serde_json", "tracing", + "web3", ] [[package]] @@ -4681,7 +4796,7 @@ dependencies = [ "futures", "hash-db", "hash256-std-hasher", - "impl-serde", + "impl-serde 0.5.0", "itertools 0.11.0", "k256", "libsecp256k1", @@ -4894,7 +5009,7 @@ version = "22.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee3b70ca340e41cde9d2e069d354508a6e37a6573d66f7cc38f11549002f64ec" dependencies = [ - "impl-serde", + "impl-serde 0.5.0", "parity-scale-codec", "ref-cast", "serde", @@ -5626,6 +5741,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -5660,7 +5781,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna", + "idna 1.0.3", "percent-encoding", ] @@ -5900,6 +6021,30 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web3" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5388522c899d1e1c96a4c307e3797e0f697ba7c77dd8e0e625ecba9dd0342937" +dependencies = [ + "arrayvec", + "derive_more 0.99.20", + "ethabi", + "ethereum-types", + "futures", + "futures-timer", + "hex", + "idna 0.4.0", + "jsonrpc-core", + "log", + "parking_lot", + "pin-project", + "rlp", + "serde", + "serde_json", + "tiny-keccak", +] + [[package]] name = "widestring" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index a28e1947..67ed25fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ tracing-subscriber = { version = "0.3.19", default-features = false, features = "json", "env-filter", ] } +web3 = { version = "0.19.0", default-features = false } # revive compiler revive-solc-json-interface = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" } diff --git a/crates/format/Cargo.toml b/crates/format/Cargo.toml index 4352683b..57422679 100644 --- a/crates/format/Cargo.toml +++ b/crates/format/Cargo.toml @@ -17,3 +17,4 @@ tracing = { workspace = true } semver = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +web3 = { workspace = true } diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index 0c9ea5f5..85a504c2 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -1,15 +1,13 @@ -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; use alloy::{ - hex, - json_abi::{Function, JsonAbi}, - primitives::{Address, Bytes, TxKind}, + json_abi::JsonAbi, + primitives::{Address, Bytes}, rpc::types::{TransactionInput, TransactionRequest}, }; -use alloy_primitives::U256; -use alloy_sol_types::SolValue; +use alloy_primitives::TxKind; use semver::VersionReq; -use serde::{Deserialize, de::Deserializer}; +use serde::Deserialize; use serde_json::Value; #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] @@ -19,7 +17,6 @@ pub struct Input { pub comment: Option, #[serde(default = "default_instance")] pub instance: String, - #[serde(deserialize_with = "deserialize_method")] pub method: Method, pub calldata: Option, pub expected: Option, @@ -47,58 +44,28 @@ pub struct ExpectedOutput { #[serde(untagged)] pub enum Calldata { Single(String), - Compound(Vec), -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum CalldataArg { - Literal(String), - /// For example: `Contract.address` - AddressRef(String), + Compound(Vec), } /// Specify how the contract is called. -#[derive(Debug, Default, Clone, Eq, PartialEq)] +#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)] pub enum Method { /// Initiate a deploy transaction, calling contracts constructor. /// /// Indicated by `#deployer`. + #[serde(rename = "#deployer")] Deployer, + /// Does not calculate and insert a function selector. /// /// Indicated by `#fallback`. #[default] + #[serde(rename = "#fallback")] Fallback, - /// Call the public function with this selector. - /// - /// Calculates the selector if neither deployer or fallback matches. - Function([u8; 4]), -} -fn deserialize_method<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - Ok(match String::deserialize(deserializer)?.as_str() { - "#deployer" => Method::Deployer, - "#fallback" => Method::Fallback, - signature => { - let signature = if signature.ends_with(')') { - signature.to_string() - } else { - format!("{signature}()") - }; - match Function::parse(&signature) { - Ok(function) => Method::Function(function.selector().0), - Err(error) => { - return Err(serde::de::Error::custom(format!( - "parsing function signature '{signature}' error: {error}" - ))); - } - } - } - }) + /// Call the public function with the given name. + #[serde(untagged)] + FunctionName(String), } impl Input { @@ -118,7 +85,7 @@ impl Input { deployed_abis: &HashMap, deployed_contracts: &HashMap, ) -> anyhow::Result { - let Method::Function(selector) = self.method else { + let Method::FunctionName(ref function_name) = self.method else { return Ok(Bytes::default()); // fallback or deployer — no input }; @@ -128,14 +95,17 @@ impl Input { tracing::trace!("ABI found for instance: {}", &self.instance); - // Find function by selector + // We follow the same logic that's implemented in the matter-labs-tester where they resolve + // the function name into a function selector and they assume that he function doesn't have + // any existing overloads. + // https://github.com/matter-labs/era-compiler-tester/blob/1dfa7d07cba0734ca97e24704f12dd57f6990c2c/compiler_tester/src/test/case/input/mod.rs#L158-L190 let function = abi .functions() - .find(|f| f.selector().0 == selector) + .find(|function| function.name.starts_with(function_name)) .ok_or_else(|| { anyhow::anyhow!( - "Function with selector {:?} not found in ABI for the instance {:?}", - selector, + "Function with name {:?} not found in ABI for the instance {:?}", + function_name, &self.instance ) })?; @@ -160,61 +130,27 @@ impl Input { &self.instance ); - let mut encoded = selector.to_vec(); - - for (i, param) in function.inputs.iter().enumerate() { - let arg = calldata_args.get(i).unwrap(); - let encoded_arg = match arg { - CalldataArg::Literal(value) => match param.ty.as_str() { - "uint256" | "uint" => { - let val: U256 = value.parse()?; - val.abi_encode() - } - "uint24" => { - let val: u32 = value.parse()?; - (val & 0xFFFFFF).abi_encode() - } - "bool" => { - let val: bool = value.parse()?; - val.abi_encode() - } - "address" => { - let addr: Address = value.parse()?; - addr.abi_encode() - } - "string" => value.abi_encode(), - "bytes32" => { - let val = hex::decode(value.trim_start_matches("0x"))?; - let mut fixed = [0u8; 32]; - fixed[..val.len()].copy_from_slice(&val); - fixed.abi_encode() - } - "uint256[]" | "uint[]" => { - let nums: Vec = serde_json::from_str(value)?; - nums.abi_encode() - } - "bytes" => { - let val = hex::decode(value.trim_start_matches("0x"))?; - val.abi_encode() - } - _ => anyhow::bail!("Unsupported type: {}", param.ty), - }, - CalldataArg::AddressRef(name) => { - let contract_name = name.trim_end_matches(".address"); - let addr = deployed_contracts - .get(contract_name) - .copied() - .ok_or_else(|| { - anyhow::anyhow!("Address for '{}' not found", contract_name) - })?; - addr.abi_encode() + // Allocating a vector that we will be using for the calldata. The vector size will be: + // 4 bytes for the function selector. + // function.inputs.len() * 32 bytes for the arguments (each argument is a U256). + let mut calldata = Vec::::with_capacity(4 + calldata_args.len() * 32); + calldata.extend(&function.selector().0); + + for (arg_idx, arg) in calldata_args.iter().enumerate() { + match resolve_argument(arg, deployed_contracts) { + Ok(resolved) => { + let mut buffer = [0u8; 32]; + resolved.to_big_endian(&mut buffer); + calldata.extend(buffer); + } + Err(error) => { + tracing::error!(arg, arg_idx, ?error, "Failed to resolve argument"); + return Err(error); } }; - - encoded.extend(encoded_arg); } - Ok(Bytes::from(encoded)) + Ok(calldata.into()) } /// Parse this input into a legacy transaction. @@ -255,12 +191,76 @@ fn default_caller() -> Address { "90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap() } +/// This function takes in the string calldata argument provided in the JSON input and resolves it +/// into a [`U256`] which is later used to construct the calldata. +/// +/// # Note +/// +/// This piece of code is taken from the matter-labs-tester repository which is licensed under MIT +/// or Apache. The original source code can be found here: +/// https://github.com/matter-labs/era-compiler-tester/blob/0ed598a27f6eceee7008deab3ff2311075a2ec69/compiler_tester/src/test/case/input/value.rs#L43-L146 +/// +/// [`U256`]: web3::types::U256 +fn resolve_argument( + value: &str, + deployed_contracts: &HashMap, +) -> anyhow::Result { + if let Some(instance) = value.strip_suffix(".address") { + Ok(web3::types::U256::from_big_endian( + deployed_contracts + .get(instance) + .ok_or_else(|| anyhow::anyhow!("Instance `{}` not found", instance))? + .as_ref(), + )) + } else if let Some(value) = value.strip_prefix('-') { + let value = web3::types::U256::from_dec_str(value) + .map_err(|error| anyhow::anyhow!("Invalid decimal literal after `-`: {}", error))?; + if value > web3::types::U256::one() << 255u8 { + anyhow::bail!("Decimal literal after `-` is too big"); + } + let value = value + .checked_sub(web3::types::U256::one()) + .ok_or_else(|| anyhow::anyhow!("`-0` is invalid literal"))?; + Ok(web3::types::U256::max_value() + .checked_sub(value) + .expect("Always valid")) + } else if let Some(value) = value.strip_prefix("0x") { + Ok(web3::types::U256::from_str(value) + .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)); + + if is_unsupported { + tracing::error!(value, "Unsupported variable used"); + anyhow::bail!("Encountered {value} which is currently unsupported by the framework"); + } else { + Ok(web3::types::U256::from_dec_str(value) + .map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?) + } + } +} + #[cfg(test)] mod tests { use super::*; use alloy::json_abi::JsonAbi; - use alloy_primitives::{address, keccak256}; + use alloy_primitives::address; + use alloy_sol_types::SolValue; use std::collections::HashMap; #[test] @@ -278,16 +278,18 @@ mod tests { "#; let parsed_abi: JsonAbi = serde_json::from_str(raw_metadata).unwrap(); - let selector = keccak256("store(uint256)".as_bytes())[0..4] - .try_into() - .unwrap(); + let selector = parsed_abi + .function("store") + .unwrap() + .first() + .unwrap() + .selector() + .0; let input = Input { instance: "Contract".to_string(), - method: Method::Function(selector), - calldata: Some(Calldata::Compound(vec![CalldataArg::Literal( - "42".to_string(), - )])), + method: Method::FunctionName("store".to_owned()), + calldata: Some(Calldata::Compound(vec!["42".into()])), ..Default::default() }; @@ -305,112 +307,6 @@ mod tests { assert_eq!(decoded.0, 42); } - #[test] - fn test_encoded_input_bool() { - let raw_abi = r#"[ - { - "inputs": [{"name": "flag", "type": "bool"}], - "name": "toggle", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } - ]"#; - - let parsed_abi: JsonAbi = serde_json::from_str(raw_abi).unwrap(); - let selector = keccak256("toggle(bool)".as_bytes())[0..4] - .try_into() - .unwrap(); - - let input = Input { - instance: "Contract".to_string(), - method: Method::Function(selector), - calldata: Some(Calldata::Compound(vec![CalldataArg::Literal( - "true".to_string(), - )])), - ..Default::default() - }; - - let mut abis = HashMap::new(); - abis.insert("Contract".to_string(), parsed_abi); - let contracts = HashMap::new(); - - let encoded = input.encoded_input(&abis, &contracts).unwrap(); - assert!(encoded.0.starts_with(&selector)); - - type T = (bool,); - let decoded: T = T::abi_decode(&encoded.0[4..]).unwrap(); - assert_eq!(decoded.0, true); - } - - #[test] - fn test_encoded_input_string() { - let raw_abi = r#"[ - { - "inputs": [{"name": "msg", "type": "string"}], - "name": "echo", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } - ]"#; - - let parsed_abi: JsonAbi = serde_json::from_str(raw_abi).unwrap(); - let selector = keccak256("echo(string)".as_bytes())[0..4] - .try_into() - .unwrap(); - - let input = Input { - instance: "Contract".to_string(), - method: Method::Function(selector), - calldata: Some(Calldata::Compound(vec![CalldataArg::Literal( - "hello".to_string(), - )])), - ..Default::default() - }; - - let mut abis = HashMap::new(); - abis.insert("Contract".to_string(), parsed_abi); - let contracts = HashMap::new(); - - let encoded = input.encoded_input(&abis, &contracts).unwrap(); - assert!(encoded.0.starts_with(&selector)); - } - - #[test] - fn test_encoded_input_uint256_array() { - let raw_abi = r#"[ - { - "inputs": [{"name": "arr", "type": "uint256[]"}], - "name": "sum", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } - ]"#; - - let parsed_abi: JsonAbi = serde_json::from_str(raw_abi).unwrap(); - let selector = keccak256("sum(uint256[])".as_bytes())[0..4] - .try_into() - .unwrap(); - - let input = Input { - instance: "Contract".to_string(), - method: Method::Function(selector), - calldata: Some(Calldata::Compound(vec![CalldataArg::Literal( - "[1,2,3]".to_string(), - )])), - ..Default::default() - }; - - let mut abis = HashMap::new(); - abis.insert("Contract".to_string(), parsed_abi); - let contracts = HashMap::new(); - - let encoded = input.encoded_input(&abis, &contracts).unwrap(); - assert!(encoded.0.starts_with(&selector)); - } - #[test] fn test_encoded_input_address() { let raw_abi = r#"[ @@ -424,16 +320,20 @@ mod tests { ]"#; let parsed_abi: JsonAbi = serde_json::from_str(raw_abi).unwrap(); - let selector = keccak256("send(address)".as_bytes())[0..4] - .try_into() - .unwrap(); + let selector = parsed_abi + .function("send") + .unwrap() + .first() + .unwrap() + .selector() + .0; let input = Input { instance: "Contract".to_string(), - method: Method::Function(selector), - calldata: Some(Calldata::Compound(vec![CalldataArg::Literal( + method: Method::FunctionName("send".to_owned()), + calldata: Some(Calldata::Compound(vec![ "0x1000000000000000000000000000000000000001".to_string(), - )])), + ])), ..Default::default() }; From 2373872230d5b5727dfcd4662fe016664c36b05e Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 00:02:48 +0300 Subject: [PATCH 07/25] Avoid extra buffer allocation --- crates/format/src/input.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index 85a504c2..af9547f4 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -133,15 +133,24 @@ impl Input { // Allocating a vector that we will be using for the calldata. The vector size will be: // 4 bytes for the function selector. // function.inputs.len() * 32 bytes for the arguments (each argument is a U256). - let mut calldata = Vec::::with_capacity(4 + calldata_args.len() * 32); - calldata.extend(&function.selector().0); + // + // We're using indices in the following code in order to avoid the need for us to allocate + // a new buffer for each one of the resolved arguments. + let mut calldata = vec![0u8; 4 + calldata_args.len() * 32]; + calldata[0..4].copy_from_slice(&function.selector().0); for (arg_idx, arg) in calldata_args.iter().enumerate() { match resolve_argument(arg, deployed_contracts) { Ok(resolved) => { - let mut buffer = [0u8; 32]; - resolved.to_big_endian(&mut buffer); - calldata.extend(buffer); + // Compute where the resolved argument will go in the call-data. Again, we're + // doing this in order to avoid performing an extra allocation for an interim + // buffer that is then just used to extend the calldata vector. In here, the 4 + // is the size of the selector which we already wrote to the calldata and the 32 + // is the size of each `U256` we're writing to the calldata. + let start_inclusive = 4 + arg_idx * 32; + let end_exclusive = start_inclusive + 32; + let slot = &mut calldata[start_inclusive..end_exclusive]; + resolved.to_big_endian(slot); } Err(error) => { tracing::error!(arg, arg_idx, ?error, "Failed to resolve argument"); From 43e0d0e59269b4bb400c87f7c0cc45de282d76a6 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 18:27:38 +0300 Subject: [PATCH 08/25] Remove reliance on the web3 crate --- Cargo.lock | 153 +------------------------------------ Cargo.toml | 1 - crates/format/Cargo.toml | 1 - crates/format/src/input.rs | 36 +++------ 4 files changed, 16 insertions(+), 175 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 81a5f8f9..9d729db5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1586,12 +1586,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "core-foundation" version = "0.9.4" @@ -1814,19 +1808,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version 0.4.1", - "syn 2.0.101", -] - [[package]] name = "derive_more" version = "1.0.0" @@ -2112,50 +2093,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "ethabi" -version = "18.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" -dependencies = [ - "ethereum-types", - "hex", - "once_cell", - "regex", - "serde", - "serde_json", - "sha3", - "thiserror 1.0.69", - "uint 0.9.5", -] - -[[package]] -name = "ethbloom" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" -dependencies = [ - "crunchy", - "fixed-hash", - "impl-rlp", - "impl-serde 0.4.0", - "tiny-keccak", -] - -[[package]] -name = "ethereum-types" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" -dependencies = [ - "ethbloom", - "fixed-hash", - "impl-rlp", - "impl-serde 0.4.0", - "primitive-types 0.12.2", - "uint 0.9.5", -] - [[package]] name = "expander" version = "2.2.1" @@ -2360,12 +2297,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - [[package]] name = "futures-util" version = "0.3.31" @@ -2828,16 +2759,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "1.0.3" @@ -2888,24 +2809,6 @@ dependencies = [ "uint 0.10.0", ] -[[package]] -name = "impl-rlp" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" -dependencies = [ - "rlp", -] - -[[package]] -name = "impl-serde" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" -dependencies = [ - "serde", -] - [[package]] name = "impl-serde" version = "0.5.0" @@ -3046,21 +2949,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonrpc-core" -version = "18.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" -dependencies = [ - "futures", - "futures-executor", - "futures-util", - "log", - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "k256" version = "0.13.4" @@ -3704,8 +3592,6 @@ checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" dependencies = [ "fixed-hash", "impl-codec 0.6.0", - "impl-rlp", - "impl-serde 0.4.0", "uint 0.9.5", ] @@ -3718,7 +3604,7 @@ dependencies = [ "fixed-hash", "impl-codec 0.7.1", "impl-num-traits", - "impl-serde 0.5.0", + "impl-serde", "scale-info", "uint 0.10.0", ] @@ -4102,7 +3988,6 @@ dependencies = [ "serde", "serde_json", "tracing", - "web3", ] [[package]] @@ -4796,7 +4681,7 @@ dependencies = [ "futures", "hash-db", "hash256-std-hasher", - "impl-serde 0.5.0", + "impl-serde", "itertools 0.11.0", "k256", "libsecp256k1", @@ -5009,7 +4894,7 @@ version = "22.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee3b70ca340e41cde9d2e069d354508a6e37a6573d66f7cc38f11549002f64ec" dependencies = [ - "impl-serde 0.5.0", + "impl-serde", "parity-scale-codec", "ref-cast", "serde", @@ -5741,12 +5626,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - [[package]] name = "unicode-ident" version = "1.0.18" @@ -5781,7 +5660,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 1.0.3", + "idna", "percent-encoding", ] @@ -6021,30 +5900,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web3" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5388522c899d1e1c96a4c307e3797e0f697ba7c77dd8e0e625ecba9dd0342937" -dependencies = [ - "arrayvec", - "derive_more 0.99.20", - "ethabi", - "ethereum-types", - "futures", - "futures-timer", - "hex", - "idna 0.4.0", - "jsonrpc-core", - "log", - "parking_lot", - "pin-project", - "rlp", - "serde", - "serde_json", - "tiny-keccak", -] - [[package]] name = "widestring" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 67ed25fa..a28e1947 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,6 @@ tracing-subscriber = { version = "0.3.19", default-features = false, features = "json", "env-filter", ] } -web3 = { version = "0.19.0", default-features = false } # revive compiler revive-solc-json-interface = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" } diff --git a/crates/format/Cargo.toml b/crates/format/Cargo.toml index 57422679..4352683b 100644 --- a/crates/format/Cargo.toml +++ b/crates/format/Cargo.toml @@ -17,4 +17,3 @@ tracing = { workspace = true } semver = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -web3 = { workspace = true } diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index af9547f4..127640ac 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, str::FromStr}; use alloy::{ json_abi::JsonAbi, - primitives::{Address, Bytes}, + primitives::{Address, Bytes, U256}, rpc::types::{TransactionInput, TransactionRequest}, }; use alloy_primitives::TxKind; @@ -136,21 +136,13 @@ impl Input { // // We're using indices in the following code in order to avoid the need for us to allocate // a new buffer for each one of the resolved arguments. - let mut calldata = vec![0u8; 4 + calldata_args.len() * 32]; - calldata[0..4].copy_from_slice(&function.selector().0); + let mut calldata = Vec::::with_capacity(4 + calldata_args.len() * 32); + calldata.extend(function.selector().0); for (arg_idx, arg) in calldata_args.iter().enumerate() { match resolve_argument(arg, deployed_contracts) { Ok(resolved) => { - // Compute where the resolved argument will go in the call-data. Again, we're - // doing this in order to avoid performing an extra allocation for an interim - // buffer that is then just used to extend the calldata vector. In here, the 4 - // is the size of the selector which we already wrote to the calldata and the 32 - // is the size of each `U256` we're writing to the calldata. - let start_inclusive = 4 + arg_idx * 32; - let end_exclusive = start_inclusive + 32; - let slot = &mut calldata[start_inclusive..end_exclusive]; - resolved.to_big_endian(slot); + calldata.extend(resolved.to_be_bytes::<32>()); } Err(error) => { tracing::error!(arg, arg_idx, ?error, "Failed to resolve argument"); @@ -208,33 +200,29 @@ fn default_caller() -> Address { /// This piece of code is taken from the matter-labs-tester repository which is licensed under MIT /// or Apache. The original source code can be found here: /// https://github.com/matter-labs/era-compiler-tester/blob/0ed598a27f6eceee7008deab3ff2311075a2ec69/compiler_tester/src/test/case/input/value.rs#L43-L146 -/// -/// [`U256`]: web3::types::U256 fn resolve_argument( value: &str, deployed_contracts: &HashMap, -) -> anyhow::Result { +) -> anyhow::Result { if let Some(instance) = value.strip_suffix(".address") { - Ok(web3::types::U256::from_big_endian( + Ok(U256::from_be_slice( deployed_contracts .get(instance) .ok_or_else(|| anyhow::anyhow!("Instance `{}` not found", instance))? .as_ref(), )) } else if let Some(value) = value.strip_prefix('-') { - let value = web3::types::U256::from_dec_str(value) + let value = U256::from_str_radix(value, 10) .map_err(|error| anyhow::anyhow!("Invalid decimal literal after `-`: {}", error))?; - if value > web3::types::U256::one() << 255u8 { + if value > U256::ONE << 255u8 { anyhow::bail!("Decimal literal after `-` is too big"); } let value = value - .checked_sub(web3::types::U256::one()) + .checked_sub(U256::ONE) .ok_or_else(|| anyhow::anyhow!("`-0` is invalid literal"))?; - Ok(web3::types::U256::max_value() - .checked_sub(value) - .expect("Always valid")) + Ok(U256::MAX.checked_sub(value).expect("Always valid")) } else if let Some(value) = value.strip_prefix("0x") { - Ok(web3::types::U256::from_str(value) + Ok(U256::from_str(value) .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 @@ -257,7 +245,7 @@ fn resolve_argument( tracing::error!(value, "Unsupported variable used"); anyhow::bail!("Encountered {value} which is currently unsupported by the framework"); } else { - Ok(web3::types::U256::from_dec_str(value) + Ok(U256::from_str_radix(value, 10) .map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?) } } From 331705134af6c447543566ecf9e54414de98eef0 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 21:13:58 +0300 Subject: [PATCH 09/25] Update the async runtime with syntactic sugar. --- Cargo.lock | 1 + Cargo.toml | 13 ++ crates/node-interaction/Cargo.toml | 1 + .../node-interaction/src/blocking_executor.rs | 203 ++++++++++++++++++ crates/node-interaction/src/lib.rs | 7 +- crates/node-interaction/src/nonce.rs | 55 ----- crates/node-interaction/src/tokio_runtime.rs | 87 -------- crates/node-interaction/src/trace.rs | 43 ---- crates/node-interaction/src/transaction.rs | 46 ---- crates/node/src/geth.rs | 23 +- crates/node/src/kitchensink.rs | 23 +- 11 files changed, 248 insertions(+), 254 deletions(-) create mode 100644 crates/node-interaction/src/blocking_executor.rs delete mode 100644 crates/node-interaction/src/nonce.rs delete mode 100644 crates/node-interaction/src/tokio_runtime.rs delete mode 100644 crates/node-interaction/src/trace.rs delete mode 100644 crates/node-interaction/src/transaction.rs diff --git a/Cargo.lock b/Cargo.lock index 42b61647..eebc4564 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4012,6 +4012,7 @@ version = "0.1.0" dependencies = [ "alloy", "anyhow", + "futures", "once_cell", "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 5d1d6a7a..a61ca4f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ alloy-primitives = "1.2.1" alloy-sol-types = "1.2.1" anyhow = "1.0" clap = { version = "4", features = ["derive"] } +futures = { version = "0.3.31" } hex = "0.4.3" reqwest = { version = "0.12.15", features = ["blocking", "json"] } once_cell = "1.21" @@ -77,3 +78,15 @@ features = [ inherits = "release" lto = true 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" diff --git a/crates/node-interaction/Cargo.toml b/crates/node-interaction/Cargo.toml index b0444564..84ea3151 100644 --- a/crates/node-interaction/Cargo.toml +++ b/crates/node-interaction/Cargo.toml @@ -11,6 +11,7 @@ rust-version.workspace = true [dependencies] alloy = { workspace = true } anyhow = { workspace = true } +futures = { workspace = true } tracing = { workspace = true } once_cell = { workspace = true } tokio = { workspace = true } diff --git a/crates/node-interaction/src/blocking_executor.rs b/crates/node-interaction/src/blocking_executor.rs new file mode 100644 index 00000000..8d882f45 --- /dev/null +++ b/crates/node-interaction/src/blocking_executor.rs @@ -0,0 +1,203 @@ +//! The alloy crate __requires__ a tokio runtime. +//! We contain any async rust right here. + +use std::{any::Any, panic::AssertUnwindSafe, pin::Pin, thread}; + +use futures::FutureExt; +use once_cell::sync::Lazy; +use tokio::{ + runtime::Builder, + sync::{mpsc::UnboundedSender, oneshot}, +}; + +/// A blocking async executor. +/// +/// This struct exposes the abstraction of a blocking async executor. It is a global and static +/// executor which means that it doesn't require for new instances of it to be created, it's a +/// singleton and can be accessed by any thread that wants to perform some async computation on the +/// blocking executor thread. +/// +/// The API of the blocking executor is created in a way so that it's very natural, simple to use, +/// and unbounded to specific tasks or return types. The following is an example of using this +/// executor to drive an async computation: +/// +/// ```rust,no_run +/// fn blocking_function() { +/// let result = BlockingExecutor::execute(async move { +/// tokio::time::sleep(std::time::Duration::from_secs(1)).await; +/// 0xFFu8 +/// }) +/// .expect("Computation failed"); +/// +/// assert_eq!(result, 0xFF); +/// } +/// ``` +/// +/// Users get to pass in their async tasks without needing to worry about putting them in a [`Box`], +/// [`Pin`], needing to perform down-casting, or the internal channel mechanism used by the runtime. +/// To the user, it just looks like a function that converts some async code into sync code. +/// +/// This struct also handled panics that occur in the passed futures and converts them into errors +/// that can be handled by the user. This is done to allow the executor to be robust. +/// +/// Internally, the executor communicates with the tokio runtime thread through channels which carry +/// the [`TaskMessage`] and the results of the execution. +pub struct BlockingExecutor; + +impl BlockingExecutor { + pub fn execute(future: impl Future + Send + 'static) -> Result + where + R: Send + 'static, + { + // A static of the state associated with the async runtime. This is initialized on the first + // access of the state. + static STATE: Lazy = Lazy::new(|| { + tracing::trace!("Initializing the BlockingExecutor state"); + + // Creating a multiple-producer-single-consumer channel which allows all of the other + // threads to communicate with this one async runtime thread. + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); + + // We spawn a new thread which will house the async runtime and will always be listening + // for new tasks coming in and executing them as they come in. + thread::spawn(move || { + // Creating the tokio runtime on this current thread. + let runtime = Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create the async runtime"); + + runtime.block_on(async move { + // Keep getting new task messages from all of the other threads. + while let Some(TaskMessage { + future: task, + response_tx: response_channel, + }) = rx.recv().await + { + // Spawn off each job so that the receive loop is not blocked. + tracing::trace!("Received a new future to execute"); + tokio::spawn(async move { + let task = AssertUnwindSafe(task).catch_unwind(); + let result = task.await; + let _ = response_channel.send(result); + }); + } + }) + }); + + // Creating the state of the async runtime. + ExecutorState { tx } + }); + + // Creating a one-shot channel for this task that will be used to send and receive the + // response of the task. + let (response_tx, response_rx) = + oneshot::channel::, Box>>(); + + // Converting the future from the shape that it is in into the shape that the runtime is + // expecting it to be in. + let future = Box::pin(async move { Box::new(future.await) as Box }); + + // Sending the task to the runtime, + let task = TaskMessage { + future, + response_tx, + }; + + if let Err(error) = STATE.tx.send(task) { + tracing::error!(?error, "Failed to send the task to the blocking executor"); + anyhow::bail!("Failed to send the task to the blocking executor: {error:?}") + } + + // Await for the result of the execution to come back over the channel. + let result = match response_rx.blocking_recv() { + Ok(result) => result, + Err(error) => { + tracing::error!( + ?error, + "Failed to get the response from the blocking executor" + ); + anyhow::bail!("Failed to get the response from the blocking executor: {error:?}") + } + }; + + match result.map(|result| { + *result + .downcast::() + .expect("Type mismatch in the downcast") + }) { + Ok(result) => Ok(result), + Err(error) => { + tracing::error!( + ?error, + "Failed to downcast the returned result into the expected type" + ); + anyhow::bail!( + "Failed to downcast the returned result into the expected type: {error:?}" + ) + } + } + } +} +/// 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. +struct ExecutorState { + /// The sending side of the task messages channel. This is used by all of the other threads to + /// communicate with the async runtime thread. + tx: UnboundedSender, +} + +/// Represents a message that contains an asynchronous task that's to be executed by the runtime +/// as well as a way for the runtime to report back on the result of the execution. +struct TaskMessage { + /// The task that's being requested to run. This is a future that returns an object that does + /// implement [`Any`] and [`Send`] to allow it to be sent between the requesting thread and the + /// async thread. + future: Pin> + Send>>, + + /// A one shot sender channel where the sender of the task is expecting to hear back on the + /// result of the task. + response_tx: oneshot::Sender, Box>>, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn simple_future_works() { + // Act + let result = BlockingExecutor::execute(async move { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + 0xFFu8 + }) + .unwrap(); + + // Assert + assert_eq!(result, 0xFFu8); + } + + #[test] + #[allow(unreachable_code, clippy::unreachable)] + fn panics_in_futures_are_caught() { + // Act + let result = BlockingExecutor::execute(async move { + panic!("This is a panic!"); + 0xFFu8 + }); + + // Assert + assert!(result.is_err()); + + // Act + let result = BlockingExecutor::execute(async move { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + 0xFFu8 + }) + .unwrap(); + + // Assert + assert_eq!(result, 0xFFu8) + } +} diff --git a/crates/node-interaction/src/lib.rs b/crates/node-interaction/src/lib.rs index 2006d1bd..2c2eef54 100644 --- a/crates/node-interaction/src/lib.rs +++ b/crates/node-interaction/src/lib.rs @@ -3,12 +3,9 @@ use alloy::primitives::Address; use alloy::rpc::types::trace::geth::{DiffMode, GethTrace}; use alloy::rpc::types::{TransactionReceipt, TransactionRequest}; -use tokio_runtime::TO_TOKIO; -pub mod nonce; -mod tokio_runtime; -pub mod trace; -pub mod transaction; +mod blocking_executor; +pub use blocking_executor::*; /// An interface for all interactions with Ethereum compatible nodes. pub trait EthereumNode { diff --git a/crates/node-interaction/src/nonce.rs b/crates/node-interaction/src/nonce.rs deleted file mode 100644 index 53b73c9a..00000000 --- a/crates/node-interaction/src/nonce.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::pin::Pin; - -use alloy::{ - primitives::Address, - providers::{Provider, ProviderBuilder}, -}; -use tokio::sync::oneshot; - -use crate::{TO_TOKIO, tokio_runtime::AsyncNodeInteraction}; - -pub type Task = Pin> + Send>>; - -pub(crate) struct Nonce { - sender: oneshot::Sender>, - task: Task, -} - -impl AsyncNodeInteraction for Nonce { - type Output = anyhow::Result; - - fn split( - self, - ) -> ( - std::pin::Pin + Send>>, - oneshot::Sender, - ) { - (self.task, self.sender) - } -} - -/// This is like `trace_transaction`, just for nonces. -pub fn fetch_onchain_nonce( - connection: String, - wallet: alloy::network::EthereumWallet, - address: Address, -) -> anyhow::Result { - let sender = TO_TOKIO.lock().unwrap().nonce_sender.clone(); - - let (tx, rx) = oneshot::channel(); - let task: Task = Box::pin(async move { - let provider = ProviderBuilder::new() - .wallet(wallet) - .connect(&connection) - .await?; - let onchain = provider.get_transaction_count(address).await?; - Ok(onchain) - }); - - sender - .blocking_send(Nonce { task, sender: tx }) - .expect("not in async context"); - - rx.blocking_recv() - .unwrap_or_else(|err| anyhow::bail!("nonce fetch failed: {err}")) -} diff --git a/crates/node-interaction/src/tokio_runtime.rs b/crates/node-interaction/src/tokio_runtime.rs deleted file mode 100644 index 20a3ae39..00000000 --- a/crates/node-interaction/src/tokio_runtime.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! The alloy crate __requires__ a tokio runtime. -//! We contain any async rust right here. - -use once_cell::sync::Lazy; -use std::pin::Pin; -use std::sync::Mutex; -use std::thread; -use tokio::runtime::Runtime; -use tokio::spawn; -use tokio::sync::{mpsc, oneshot}; -use tokio::task::JoinError; - -use crate::nonce::Nonce; -use crate::trace::Trace; -use crate::transaction::Transaction; - -pub(crate) static TO_TOKIO: Lazy> = - Lazy::new(|| Mutex::new(TokioRuntime::spawn())); - -/// Common interface for executing async node interactions from a non-async context. -#[allow(clippy::type_complexity)] -pub(crate) trait AsyncNodeInteraction: Send + 'static { - type Output: Send; - - //// Returns the task and the output sender. - fn split( - self, - ) -> ( - Pin + Send>>, - oneshot::Sender, - ); -} - -pub(crate) struct TokioRuntime { - pub(crate) transaction_sender: mpsc::Sender, - pub(crate) trace_sender: mpsc::Sender, - pub(crate) nonce_sender: mpsc::Sender, -} - -impl TokioRuntime { - fn spawn() -> Self { - let rt = Runtime::new().expect("should be able to create the tokio runtime"); - let (transaction_sender, transaction_receiver) = mpsc::channel::(1024); - let (trace_sender, trace_receiver) = mpsc::channel::(1024); - let (nonce_sender, nonce_receiver) = mpsc::channel::(1024); - - thread::spawn(move || { - rt.block_on(async move { - let transaction_task = spawn(interaction::(transaction_receiver)); - let trace_task = spawn(interaction::(trace_receiver)); - let nonce_task = spawn(interaction::(nonce_receiver)); - - if let Err(error) = transaction_task.await { - tracing::error!("tokio transaction task failed: {error}"); - } - if let Err(error) = trace_task.await { - tracing::error!("tokio trace transaction task failed: {error}"); - } - if let Err(error) = nonce_task.await { - tracing::error!("tokio nonce task failed: {error}"); - } - }); - }); - - Self { - transaction_sender, - trace_sender, - nonce_sender, - } - } -} - -async fn interaction(mut receiver: mpsc::Receiver) -> Result<(), JoinError> -where - T: AsyncNodeInteraction, -{ - while let Some(task) = receiver.recv().await { - spawn(async move { - let (task, sender) = task.split(); - sender - .send(task.await) - .unwrap_or_else(|_| panic!("failed to send task output")); - }); - } - - Ok(()) -} diff --git a/crates/node-interaction/src/trace.rs b/crates/node-interaction/src/trace.rs deleted file mode 100644 index 9255d007..00000000 --- a/crates/node-interaction/src/trace.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Trace transactions in a sync context. - -use std::pin::Pin; - -use alloy::rpc::types::trace::geth::GethTrace; -use tokio::sync::oneshot; - -use crate::TO_TOKIO; -use crate::tokio_runtime::AsyncNodeInteraction; - -pub type Task = Pin> + Send>>; - -pub(crate) struct Trace { - sender: oneshot::Sender>, - task: Task, -} - -impl AsyncNodeInteraction for Trace { - type Output = anyhow::Result; - - fn split( - self, - ) -> ( - std::pin::Pin + Send>>, - oneshot::Sender, - ) { - (self.task, self.sender) - } -} - -/// Execute some [Task] that return a [GethTrace] result. -pub fn trace_transaction(task: Task) -> anyhow::Result { - let task_sender = TO_TOKIO.lock().unwrap().trace_sender.clone(); - let (sender, receiver) = oneshot::channel(); - - task_sender - .blocking_send(Trace { task, sender }) - .expect("we are not calling this from an async context"); - - receiver - .blocking_recv() - .unwrap_or_else(|error| anyhow::bail!("no trace received: {error}")) -} diff --git a/crates/node-interaction/src/transaction.rs b/crates/node-interaction/src/transaction.rs deleted file mode 100644 index b5af2212..00000000 --- a/crates/node-interaction/src/transaction.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Execute transactions in a sync context. - -use std::pin::Pin; - -use alloy::rpc::types::TransactionReceipt; -use tokio::sync::oneshot; - -use crate::TO_TOKIO; -use crate::tokio_runtime::AsyncNodeInteraction; - -pub type Task = Pin> + Send>>; - -pub(crate) struct Transaction { - receipt_sender: oneshot::Sender>, - task: Task, -} - -impl AsyncNodeInteraction for Transaction { - type Output = anyhow::Result; - - fn split( - self, - ) -> ( - Pin + Send>>, - oneshot::Sender, - ) { - (self.task, self.receipt_sender) - } -} - -/// Execute some [Task] that returns a [TransactionReceipt]. -pub fn execute_transaction(task: Task) -> anyhow::Result { - let request_sender = TO_TOKIO.lock().unwrap().transaction_sender.clone(); - let (receipt_sender, receipt_receiver) = oneshot::channel(); - - request_sender - .blocking_send(Transaction { - receipt_sender, - task, - }) - .expect("we are not calling this from an async context"); - - receipt_receiver - .blocking_recv() - .unwrap_or_else(|error| anyhow::bail!("no receipt received: {error}")) -} diff --git a/crates/node/src/geth.rs b/crates/node/src/geth.rs index 99893a05..57cb2dba 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -23,10 +23,7 @@ use alloy::{ }, }; use revive_dt_config::Arguments; -use revive_dt_node_interaction::{ - EthereumNode, nonce::fetch_onchain_nonce, trace::trace_transaction, - transaction::execute_transaction, -}; +use revive_dt_node_interaction::{BlockingExecutor, EthereumNode}; use tracing::Level; use crate::Node; @@ -205,7 +202,7 @@ impl EthereumNode for Instance { let connection_string = self.connection_string(); let wallet = self.wallet.clone(); - execute_transaction(Box::pin(async move { + BlockingExecutor::execute(async move { let outer_span = tracing::debug_span!("Submitting transaction", ?transaction,); let _outer_guard = outer_span.enter(); @@ -284,7 +281,7 @@ impl EthereumNode for Instance { } } } - })) + })? } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] @@ -300,14 +297,14 @@ impl EthereumNode for Instance { }); let wallet = self.wallet.clone(); - trace_transaction(Box::pin(async move { + BlockingExecutor::execute(async move { Ok(ProviderBuilder::new() .wallet(wallet) .connect(&connection_string) .await? .debug_trace_transaction(transaction.transaction_hash, trace_options) .await?) - })) + })? } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] @@ -329,7 +326,15 @@ impl EthereumNode for Instance { let connection_string = self.connection_string.clone(); let wallet = self.wallet.clone(); - let onchain_nonce = fetch_onchain_nonce(connection_string, wallet, address)?; + let onchain_nonce = BlockingExecutor::execute::>(async move { + ProviderBuilder::new() + .wallet(wallet) + .connect(&connection_string) + .await? + .get_transaction_count(address) + .await + .map_err(Into::into) + })??; let mut nonces = self.nonces.lock().unwrap(); let current = nonces.entry(address).or_insert(onchain_nonce); diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index c05f54cf..768c1aaf 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -27,10 +27,7 @@ use sp_runtime::AccountId32; use tracing::Level; use revive_dt_config::Arguments; -use revive_dt_node_interaction::{ - EthereumNode, nonce::fetch_onchain_nonce, trace::trace_transaction, - transaction::execute_transaction, -}; +use revive_dt_node_interaction::{BlockingExecutor, EthereumNode}; use crate::Node; @@ -340,7 +337,7 @@ impl EthereumNode for KitchensinkNode { tracing::debug!("Submitting transaction: {transaction:#?}"); tracing::info!("Submitting tx to kitchensink"); - let receipt = execute_transaction(Box::pin(async move { + let receipt = BlockingExecutor::execute(async move { Ok(ProviderBuilder::new() .wallet(wallet) .connect(&url) @@ -349,7 +346,7 @@ impl EthereumNode for KitchensinkNode { .await? .get_receipt() .await?) - })); + })?; tracing::info!(?receipt, "Submitted tx to kitchensink"); receipt } @@ -368,14 +365,14 @@ impl EthereumNode for KitchensinkNode { let wallet = self.wallet.clone(); - trace_transaction(Box::pin(async move { + BlockingExecutor::execute(async move { Ok(ProviderBuilder::new() .wallet(wallet) .connect(&url) .await? .debug_trace_transaction(transaction.transaction_hash, trace_options) .await?) - })) + })? } #[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))] @@ -394,7 +391,15 @@ impl EthereumNode for KitchensinkNode { let url = self.rpc_url.clone(); let wallet = self.wallet.clone(); - let onchain_nonce = fetch_onchain_nonce(url, wallet, address)?; + let onchain_nonce = BlockingExecutor::execute::>(async move { + ProviderBuilder::new() + .wallet(wallet) + .connect(&url) + .await? + .get_transaction_count(address) + .await + .map_err(Into::into) + })??; let mut nonces = self.nonces.lock().unwrap(); let current = nonces.entry(address).or_insert(onchain_nonce); From 83c20b1be3c77ddc32f0e4966d9608767b4d4fe7 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 21:30:35 +0300 Subject: [PATCH 10/25] Fix tests --- crates/format/src/input.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index 0e4b802d..270d908f 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, str::FromStr}; +use std::collections::HashMap; use alloy::{ json_abi::JsonAbi, @@ -210,7 +210,7 @@ fn resolve_argument( .ok_or_else(|| anyhow::anyhow!("`-0` is invalid literal"))?; Ok(U256::MAX.checked_sub(value).expect("Always valid")) } else if let Some(value) = value.strip_prefix("0x") { - Ok(U256::from_str(value) + 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 @@ -313,7 +313,7 @@ mod tests { .selector() .0; - let input = Input { + let input: Input = Input { instance: "Contract".to_string(), method: Method::FunctionName("send".to_owned()), calldata: Some(Calldata::Compound(vec![ From 27a0a0de0bd057cada658766e14c2485ad370558 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 21:33:57 +0300 Subject: [PATCH 11/25] Fix doc test --- crates/node-interaction/src/blocking_executor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/node-interaction/src/blocking_executor.rs b/crates/node-interaction/src/blocking_executor.rs index 8d882f45..53123b52 100644 --- a/crates/node-interaction/src/blocking_executor.rs +++ b/crates/node-interaction/src/blocking_executor.rs @@ -21,7 +21,9 @@ use tokio::{ /// and unbounded to specific tasks or return types. The following is an example of using this /// executor to drive an async computation: /// -/// ```rust,no_run +/// ```rust +/// use revive_dt_node_interaction::*; +/// /// fn blocking_function() { /// let result = BlockingExecutor::execute(async move { /// tokio::time::sleep(std::time::Duration::from_secs(1)).await; From 7d48d1600e97b3b1bfd1cad59e94e563559fae90 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 21:59:44 +0300 Subject: [PATCH 12/25] Give nodes a standard way to get their alloy provider --- crates/node/src/geth.rs | 50 ++++++++++++++++++------------- crates/node/src/kitchensink.rs | 54 +++++++++++++++++++--------------- 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/crates/node/src/geth.rs b/crates/node/src/geth.rs index 57cb2dba..52a8aed0 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -14,9 +14,13 @@ use std::{ }; use alloy::{ - network::EthereumWallet, + network::{Ethereum, EthereumWallet}, primitives::Address, - providers::{Provider, ProviderBuilder, ext::DebugApi}, + providers::{ + Provider, ProviderBuilder, + ext::DebugApi, + fillers::{FillProvider, TxFiller}, + }, rpc::types::{ TransactionReceipt, TransactionRequest, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, @@ -191,6 +195,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 +221,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 +306,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 +337,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 diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index 768c1aaf..b8586a9a 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -13,9 +13,13 @@ use std::{ use alloy::{ hex, - network::EthereumWallet, + network::{Ethereum, EthereumWallet}, primitives::Address, - providers::{Provider, ProviderBuilder, ext::DebugApi}, + providers::{ + Provider, ProviderBuilder, + ext::DebugApi, + fillers::{FillProvider, TxFiller}, + }, rpc::types::{ TransactionReceipt, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, @@ -323,6 +327,24 @@ 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 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 KitchensinkNode { @@ -331,16 +353,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() - .wallet(wallet) - .connect(&url) + Ok(provider .await? .send_transaction(transaction) .await? @@ -356,19 +372,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() - .wallet(wallet) - .connect(&url) + Ok(provider .await? .debug_trace_transaction(transaction.transaction_hash, trace_options) .await?) @@ -388,13 +400,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 From a4f5c4c8af8c07cbbcaf325cd9685e0c7b414b72 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 22:16:37 +0300 Subject: [PATCH 13/25] Add ability to get the chain_id from node --- crates/node-interaction/src/lib.rs | 5 ++++- crates/node/src/geth.rs | 33 ++++++++++++++++++++++++++++- crates/node/src/kitchensink.rs | 34 +++++++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/crates/node-interaction/src/lib.rs b/crates/node-interaction/src/lib.rs index 2c2eef54..d73d838e 100644 --- a/crates/node-interaction/src/lib.rs +++ b/crates/node-interaction/src/lib.rs @@ -1,6 +1,6 @@ //! This crate implements all node interactions. -use alloy::primitives::Address; +use alloy::primitives::{Address, ChainId}; use alloy::rpc::types::trace::geth::{DiffMode, GethTrace}; use alloy::rpc::types::{TransactionReceipt, TransactionRequest}; @@ -23,4 +23,7 @@ pub trait EthereumNode { /// Returns the next available nonce for the given [Address]. fn fetch_add_nonce(&self, address: Address) -> anyhow::Result; + + /// Returns the ID of the chain that the node is on. + fn chain_id(&self) -> anyhow::Result; } diff --git a/crates/node/src/geth.rs b/crates/node/src/geth.rs index 52a8aed0..e6f500a4 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -352,6 +352,14 @@ 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) + })? + } } impl Node for Instance { @@ -439,7 +447,7 @@ mod tests { use crate::{GENESIS_JSON, Node}; - use super::Instance; + use super::*; fn test_config() -> (Arguments, TempDir) { let mut config = Arguments::default(); @@ -449,6 +457,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) @@ -471,4 +489,17 @@ 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); + } } diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index b8586a9a..9a186fb4 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -229,6 +229,7 @@ impl KitchensinkNode { Ok(()) } + #[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))] fn extract_balance_from_genesis_file( &self, @@ -415,6 +416,14 @@ 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) + })? + } } impl Node for KitchensinkNode { @@ -507,7 +516,7 @@ mod tests { use std::fs; - use super::KitchensinkNode; + use super::*; use crate::{GENESIS_JSON, Node}; fn test_config() -> (Arguments, TempDir) { @@ -522,6 +531,16 @@ mod tests { (config, temp_dir) } + fn new_node() -> (KitchensinkNode, TempDir) { + 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, temp_dir) + } + #[test] fn test_init_generates_chainspec_with_balances() { let genesis_content = r#" @@ -683,4 +702,17 @@ mod tests { "Expected eth-rpc 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); + } } From 8f80b1da8ab3da08768671845f074b9fc983a729 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 22:30:30 +0300 Subject: [PATCH 14/25] Get kitchensink provider to use kitchensink network --- crates/node/src/kitchensink.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index bdc58611..2510995b 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -340,13 +340,18 @@ impl KitchensinkNode { &self, ) -> impl Future< Output = anyhow::Result< - FillProvider, impl Provider, Ethereum>, + 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 From fa4bbbb98757aab88e833784aab24004755a13b9 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 22:33:09 +0300 Subject: [PATCH 15/25] Use provider method in tests --- crates/node/src/kitchensink.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index 2510995b..eca3a772 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -983,12 +983,7 @@ mod tests { node.spawn(GENESIS_JSON.to_owned()) .expect("Failed to spawn the 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() From 61540741e11f173718fabd118fc0a0622cba529f Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 22:37:50 +0300 Subject: [PATCH 16/25] Add support for getting the gas limit from the node --- crates/node-interaction/src/lib.rs | 5 +++++ crates/node/src/geth.rs | 27 +++++++++++++++++++++++++++ crates/node/src/kitchensink.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/crates/node-interaction/src/lib.rs b/crates/node-interaction/src/lib.rs index d73d838e..2985ccaf 100644 --- a/crates/node-interaction/src/lib.rs +++ b/crates/node-interaction/src/lib.rs @@ -26,4 +26,9 @@ pub trait EthereumNode { /// Returns the ID of the chain that the node is on. fn chain_id(&self) -> anyhow::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 last block. + fn gas_limit(&self) -> anyhow::Result; } diff --git a/crates/node/src/geth.rs b/crates/node/src/geth.rs index e6f500a4..cb506fa0 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -14,6 +14,7 @@ use std::{ }; use alloy::{ + eips::BlockNumberOrTag, network::{Ethereum, EthereumWallet}, primitives::Address, providers::{ @@ -360,6 +361,19 @@ impl EthereumNode for Instance { provider.await?.get_chain_id().await.map_err(Into::into) })? } + + #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] + fn gas_limit(&self) -> anyhow::Result { + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider + .await? + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .ok_or(anyhow::Error::msg("Blockchain has no blocks")) + .map(|block| block.header.gas_limit as _) + })? + } } impl Node for Instance { @@ -502,4 +516,17 @@ mod tests { 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.gas_limit(); + + // Assert + let gas_limit = gas_limit.expect("Failed to get the gas limit"); + assert_eq!(gas_limit, u32::MAX as u128) + } } diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index eca3a772..74e810aa 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -13,6 +13,7 @@ use std::{ use alloy::{ consensus::{BlockHeader, TxEnvelope}, + eips::BlockNumberOrTag, hex, network::{ Ethereum, EthereumWallet, Network, TransactionBuilder, TransactionBuilderError, @@ -436,6 +437,19 @@ impl EthereumNode for KitchensinkNode { provider.await?.get_chain_id().await.map_err(Into::into) })? } + + #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] + fn gas_limit(&self) -> anyhow::Result { + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider + .await? + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .ok_or(anyhow::Error::msg("Blockchain has no blocks")) + .map(|block| block.header.gas_limit) + })? + } } impl Node for KitchensinkNode { @@ -1175,4 +1189,17 @@ mod tests { 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.gas_limit(); + + // Assert + let gas_limit = gas_limit.expect("Failed to get the gas limit"); + assert_eq!(gas_limit, 52430300000000000000) + } } From 02547b62ee76dab64631328809f45cda07c38fd9 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 22:48:45 +0300 Subject: [PATCH 17/25] Add a way to get the coinbase address --- crates/node-interaction/src/lib.rs | 19 ++++++++++--------- crates/node/src/geth.rs | 26 ++++++++++++++++++++++++++ crates/node/src/kitchensink.rs | 26 ++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/crates/node-interaction/src/lib.rs b/crates/node-interaction/src/lib.rs index 2985ccaf..f0548426 100644 --- a/crates/node-interaction/src/lib.rs +++ b/crates/node-interaction/src/lib.rs @@ -3,6 +3,7 @@ use alloy::primitives::{Address, ChainId}; 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,25 +11,25 @@ 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) -> anyhow::Result; + 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 last block. - fn gas_limit(&self) -> anyhow::Result; + fn gas_limit(&self) -> Result; + + /// Returns the coinbase of the last block. + fn coinbase(&self) -> Result
; } diff --git a/crates/node/src/geth.rs b/crates/node/src/geth.rs index cb506fa0..0a18e6fa 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -374,6 +374,19 @@ impl EthereumNode for Instance { .map(|block| block.header.gas_limit as _) })? } + + #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] + fn coinbase(&self) -> anyhow::Result
{ + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider + .await? + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .ok_or(anyhow::Error::msg("Blockchain has no blocks")) + .map(|block| block.header.beneficiary) + })? + } } impl Node for Instance { @@ -529,4 +542,17 @@ mod tests { 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.coinbase(); + + // Assert + let coinbase = coinbase.expect("Failed to get the gas limit"); + assert_eq!(coinbase, Address::new([0xFF; 20])) + } } diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index 74e810aa..f055ae12 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -450,6 +450,19 @@ impl EthereumNode for KitchensinkNode { .map(|block| block.header.gas_limit) })? } + + #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] + fn coinbase(&self) -> anyhow::Result
{ + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider + .await? + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .ok_or(anyhow::Error::msg("Blockchain has no blocks")) + .map(|block| block.header.beneficiary) + })? + } } impl Node for KitchensinkNode { @@ -1202,4 +1215,17 @@ mod tests { let gas_limit = gas_limit.expect("Failed to get the gas limit"); assert_eq!(gas_limit, 52430300000000000000) } + + #[test] + fn can_get_coinbase_from_node() { + // Arrange + let (node, _temp_dir) = new_node(); + + // Act + let coinbase = node.coinbase(); + + // Assert + let coinbase = coinbase.expect("Failed to get the gas limit"); + assert_eq!(coinbase, Address::ZERO) + } } From ddd775d703ff1b327a95cc6e988d9ad44a4de2f5 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 22:53:40 +0300 Subject: [PATCH 18/25] Add a way to get the block difficulty from the node --- crates/node-interaction/src/lib.rs | 9 ++++--- crates/node/src/geth.rs | 38 +++++++++++++++++++++++++----- crates/node/src/kitchensink.rs | 36 ++++++++++++++++++++++++---- 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/crates/node-interaction/src/lib.rs b/crates/node-interaction/src/lib.rs index f0548426..fdb21d83 100644 --- a/crates/node-interaction/src/lib.rs +++ b/crates/node-interaction/src/lib.rs @@ -1,6 +1,6 @@ //! This crate implements all node interactions. -use alloy::primitives::{Address, ChainId}; +use alloy::primitives::{Address, ChainId, U256}; use alloy::rpc::types::trace::geth::{DiffMode, GethTrace}; use alloy::rpc::types::{TransactionReceipt, TransactionRequest}; use anyhow::Result; @@ -28,8 +28,11 @@ pub trait EthereumNode { // 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 last block. - fn gas_limit(&self) -> Result; + fn block_gas_limit(&self) -> Result; /// Returns the coinbase of the last block. - fn coinbase(&self) -> Result
; + fn block_coinbase(&self) -> Result
; + + /// Returns the difficulty of the last block. + fn block_difficulty(&self) -> Result; } diff --git a/crates/node/src/geth.rs b/crates/node/src/geth.rs index 0a18e6fa..3c4a54dc 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -16,7 +16,7 @@ use std::{ use alloy::{ eips::BlockNumberOrTag, network::{Ethereum, EthereumWallet}, - primitives::Address, + primitives::{Address, U256}, providers::{ Provider, ProviderBuilder, ext::DebugApi, @@ -363,7 +363,7 @@ impl EthereumNode for Instance { } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] - fn gas_limit(&self) -> anyhow::Result { + fn block_gas_limit(&self) -> anyhow::Result { let provider = self.provider(); BlockingExecutor::execute(async move { provider @@ -376,7 +376,7 @@ impl EthereumNode for Instance { } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] - fn coinbase(&self) -> anyhow::Result
{ + fn block_coinbase(&self) -> anyhow::Result
{ let provider = self.provider(); BlockingExecutor::execute(async move { provider @@ -387,6 +387,19 @@ impl EthereumNode for Instance { .map(|block| block.header.beneficiary) })? } + + #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] + fn block_difficulty(&self) -> anyhow::Result { + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider + .await? + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .ok_or(anyhow::Error::msg("Blockchain has no blocks")) + .map(|block| block.header.difficulty) + })? + } } impl Node for Instance { @@ -536,7 +549,7 @@ mod tests { let (node, _temp_dir) = new_node(); // Act - let gas_limit = node.gas_limit(); + let gas_limit = node.block_gas_limit(); // Assert let gas_limit = gas_limit.expect("Failed to get the gas limit"); @@ -549,10 +562,23 @@ mod tests { let (node, _temp_dir) = new_node(); // Act - let coinbase = node.coinbase(); + let coinbase = node.block_coinbase(); // Assert - let coinbase = coinbase.expect("Failed to get the gas limit"); + 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(); + + // Assert + let block_difficulty = block_difficulty.expect("Failed to get the block difficulty"); + assert_eq!(block_difficulty, U256::ZERO) + } } diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index f055ae12..957eec85 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -439,7 +439,7 @@ impl EthereumNode for KitchensinkNode { } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] - fn gas_limit(&self) -> anyhow::Result { + fn block_gas_limit(&self) -> anyhow::Result { let provider = self.provider(); BlockingExecutor::execute(async move { provider @@ -452,7 +452,7 @@ impl EthereumNode for KitchensinkNode { } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] - fn coinbase(&self) -> anyhow::Result
{ + fn block_coinbase(&self) -> anyhow::Result
{ let provider = self.provider(); BlockingExecutor::execute(async move { provider @@ -463,6 +463,19 @@ impl EthereumNode for KitchensinkNode { .map(|block| block.header.beneficiary) })? } + + #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] + fn block_difficulty(&self) -> anyhow::Result { + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider + .await? + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .ok_or(anyhow::Error::msg("Blockchain has no blocks")) + .map(|block| block.header.difficulty) + })? + } } impl Node for KitchensinkNode { @@ -1209,7 +1222,7 @@ mod tests { let (node, _temp_dir) = new_node(); // Act - let gas_limit = node.gas_limit(); + let gas_limit = node.block_gas_limit(); // Assert let gas_limit = gas_limit.expect("Failed to get the gas limit"); @@ -1222,10 +1235,23 @@ mod tests { let (node, _temp_dir) = new_node(); // Act - let coinbase = node.coinbase(); + let coinbase = node.block_coinbase(); // Assert - let coinbase = coinbase.expect("Failed to get the gas limit"); + 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, _temp_dir) = new_node(); + + // Act + let block_difficulty = node.block_difficulty(); + + // Assert + let block_difficulty = block_difficulty.expect("Failed to get the block difficulty"); + assert_eq!(block_difficulty, U256::ZERO) + } } From 68bda92465e5ac0dcbe691c4cfc023cbce91911d Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 23:14:37 +0300 Subject: [PATCH 19/25] Add a way to get block info from the node --- crates/node-interaction/src/lib.rs | 11 ++++- crates/node/src/geth.rs | 73 +++++++++++++++++++++++++++++- crates/node/src/kitchensink.rs | 73 +++++++++++++++++++++++++++++- 3 files changed, 154 insertions(+), 3 deletions(-) diff --git a/crates/node-interaction/src/lib.rs b/crates/node-interaction/src/lib.rs index fdb21d83..284f9328 100644 --- a/crates/node-interaction/src/lib.rs +++ b/crates/node-interaction/src/lib.rs @@ -1,6 +1,6 @@ //! This crate implements all node interactions. -use alloy::primitives::{Address, ChainId, U256}; +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; @@ -35,4 +35,13 @@ pub trait EthereumNode { /// Returns the difficulty of the last block. fn block_difficulty(&self) -> Result; + + /// Returns the hash of the last block. + fn block_hash(&self) -> Result; + + /// Returns the timestamp of the last block, + fn block_timestamp(&self) -> 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 3c4a54dc..02abf621 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -16,7 +16,7 @@ use std::{ use alloy::{ eips::BlockNumberOrTag, network::{Ethereum, EthereumWallet}, - primitives::{Address, U256}, + primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, U256}, providers::{ Provider, ProviderBuilder, ext::DebugApi, @@ -400,6 +400,40 @@ impl EthereumNode for Instance { .map(|block| block.header.difficulty) })? } + + #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] + fn block_hash(&self) -> anyhow::Result { + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider + .await? + .get_block_by_number(BlockNumberOrTag::Latest) + .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) -> anyhow::Result { + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider + .await? + .get_block_by_number(BlockNumberOrTag::Latest) + .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 { @@ -581,4 +615,41 @@ mod tests { 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(); + + // 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(); + + // 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 957eec85..b32464ac 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -19,7 +19,7 @@ use alloy::{ Ethereum, EthereumWallet, Network, TransactionBuilder, TransactionBuilderError, UnbuiltTransactionError, }, - primitives::{Address, B64, B256, BlockNumber, Bloom, Bytes, U256}, + primitives::{Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, U256}, providers::{ Provider, ProviderBuilder, ext::DebugApi, @@ -476,6 +476,40 @@ impl EthereumNode for KitchensinkNode { .map(|block| block.header.difficulty) })? } + + #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] + fn block_hash(&self) -> anyhow::Result { + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider + .await? + .get_block_by_number(BlockNumberOrTag::Latest) + .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) -> anyhow::Result { + let provider = self.provider(); + BlockingExecutor::execute(async move { + provider + .await? + .get_block_by_number(BlockNumberOrTag::Latest) + .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 { @@ -1254,4 +1288,41 @@ mod tests { 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(); + + // 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(); + + // 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) + } } From 6d7cd679311e8f77365ba259cc14cf8ad4abc6f0 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 23:21:53 +0300 Subject: [PATCH 20/25] Expose APIs for getting the info of a specific block --- crates/node-interaction/src/lib.rs | 21 +++++++++++---------- crates/node/src/geth.rs | 30 +++++++++++++++--------------- crates/node/src/kitchensink.rs | 30 +++++++++++++++--------------- 3 files changed, 41 insertions(+), 40 deletions(-) diff --git a/crates/node-interaction/src/lib.rs b/crates/node-interaction/src/lib.rs index 284f9328..afba76a9 100644 --- a/crates/node-interaction/src/lib.rs +++ b/crates/node-interaction/src/lib.rs @@ -1,5 +1,6 @@ //! This crate implements all node interactions. +use alloy::eips::BlockNumberOrTag; use alloy::primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, ChainId, U256}; use alloy::rpc::types::trace::geth::{DiffMode, GethTrace}; use alloy::rpc::types::{TransactionReceipt, TransactionRequest}; @@ -27,20 +28,20 @@ pub trait EthereumNode { // 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 last block. - fn block_gas_limit(&self) -> Result; + /// Returns the gas limit of the specified block. + fn block_gas_limit(&self, number: BlockNumberOrTag) -> Result; - /// Returns the coinbase of the last block. - fn block_coinbase(&self) -> Result
; + /// Returns the coinbase of the specified block. + fn block_coinbase(&self, number: BlockNumberOrTag) -> Result
; - /// Returns the difficulty of the last block. - fn block_difficulty(&self) -> Result; + /// Returns the difficulty of the specified block. + fn block_difficulty(&self, number: BlockNumberOrTag) -> Result; - /// Returns the hash of the last block. - fn block_hash(&self) -> Result; + /// Returns the hash of the specified block. + fn block_hash(&self, number: BlockNumberOrTag) -> Result; - /// Returns the timestamp of the last block, - fn block_timestamp(&self) -> 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 02abf621..4efd626e 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -363,12 +363,12 @@ impl EthereumNode for Instance { } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] - fn block_gas_limit(&self) -> anyhow::Result { + fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result { let provider = self.provider(); BlockingExecutor::execute(async move { provider .await? - .get_block_by_number(BlockNumberOrTag::Latest) + .get_block_by_number(number) .await? .ok_or(anyhow::Error::msg("Blockchain has no blocks")) .map(|block| block.header.gas_limit as _) @@ -376,12 +376,12 @@ impl EthereumNode for Instance { } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] - fn block_coinbase(&self) -> anyhow::Result
{ + fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result
{ let provider = self.provider(); BlockingExecutor::execute(async move { provider .await? - .get_block_by_number(BlockNumberOrTag::Latest) + .get_block_by_number(number) .await? .ok_or(anyhow::Error::msg("Blockchain has no blocks")) .map(|block| block.header.beneficiary) @@ -389,12 +389,12 @@ impl EthereumNode for Instance { } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] - fn block_difficulty(&self) -> anyhow::Result { + fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result { let provider = self.provider(); BlockingExecutor::execute(async move { provider .await? - .get_block_by_number(BlockNumberOrTag::Latest) + .get_block_by_number(number) .await? .ok_or(anyhow::Error::msg("Blockchain has no blocks")) .map(|block| block.header.difficulty) @@ -402,12 +402,12 @@ impl EthereumNode for Instance { } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] - fn block_hash(&self) -> anyhow::Result { + fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result { let provider = self.provider(); BlockingExecutor::execute(async move { provider .await? - .get_block_by_number(BlockNumberOrTag::Latest) + .get_block_by_number(number) .await? .ok_or(anyhow::Error::msg("Blockchain has no blocks")) .map(|block| block.header.hash) @@ -415,12 +415,12 @@ impl EthereumNode for Instance { } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] - fn block_timestamp(&self) -> anyhow::Result { + fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result { let provider = self.provider(); BlockingExecutor::execute(async move { provider .await? - .get_block_by_number(BlockNumberOrTag::Latest) + .get_block_by_number(number) .await? .ok_or(anyhow::Error::msg("Blockchain has no blocks")) .map(|block| block.header.timestamp) @@ -583,7 +583,7 @@ mod tests { let (node, _temp_dir) = new_node(); // Act - let gas_limit = node.block_gas_limit(); + let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest); // Assert let gas_limit = gas_limit.expect("Failed to get the gas limit"); @@ -596,7 +596,7 @@ mod tests { let (node, _temp_dir) = new_node(); // Act - let coinbase = node.block_coinbase(); + let coinbase = node.block_coinbase(BlockNumberOrTag::Latest); // Assert let coinbase = coinbase.expect("Failed to get the coinbase"); @@ -609,7 +609,7 @@ mod tests { let (node, _temp_dir) = new_node(); // Act - let block_difficulty = node.block_difficulty(); + let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest); // Assert let block_difficulty = block_difficulty.expect("Failed to get the block difficulty"); @@ -622,7 +622,7 @@ mod tests { let (node, _temp_dir) = new_node(); // Act - let block_hash = node.block_hash(); + let block_hash = node.block_hash(BlockNumberOrTag::Latest); // Assert let _ = block_hash.expect("Failed to get the block hash"); @@ -634,7 +634,7 @@ mod tests { let (node, _temp_dir) = new_node(); // Act - let block_timestamp = node.block_timestamp(); + let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest); // Assert let _ = block_timestamp.expect("Failed to get the block timestamp"); diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index b32464ac..d003f388 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -439,12 +439,12 @@ impl EthereumNode for KitchensinkNode { } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] - fn block_gas_limit(&self) -> anyhow::Result { + fn block_gas_limit(&self, number: BlockNumberOrTag) -> anyhow::Result { let provider = self.provider(); BlockingExecutor::execute(async move { provider .await? - .get_block_by_number(BlockNumberOrTag::Latest) + .get_block_by_number(number) .await? .ok_or(anyhow::Error::msg("Blockchain has no blocks")) .map(|block| block.header.gas_limit) @@ -452,12 +452,12 @@ impl EthereumNode for KitchensinkNode { } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] - fn block_coinbase(&self) -> anyhow::Result
{ + fn block_coinbase(&self, number: BlockNumberOrTag) -> anyhow::Result
{ let provider = self.provider(); BlockingExecutor::execute(async move { provider .await? - .get_block_by_number(BlockNumberOrTag::Latest) + .get_block_by_number(number) .await? .ok_or(anyhow::Error::msg("Blockchain has no blocks")) .map(|block| block.header.beneficiary) @@ -465,12 +465,12 @@ impl EthereumNode for KitchensinkNode { } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] - fn block_difficulty(&self) -> anyhow::Result { + fn block_difficulty(&self, number: BlockNumberOrTag) -> anyhow::Result { let provider = self.provider(); BlockingExecutor::execute(async move { provider .await? - .get_block_by_number(BlockNumberOrTag::Latest) + .get_block_by_number(number) .await? .ok_or(anyhow::Error::msg("Blockchain has no blocks")) .map(|block| block.header.difficulty) @@ -478,12 +478,12 @@ impl EthereumNode for KitchensinkNode { } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] - fn block_hash(&self) -> anyhow::Result { + fn block_hash(&self, number: BlockNumberOrTag) -> anyhow::Result { let provider = self.provider(); BlockingExecutor::execute(async move { provider .await? - .get_block_by_number(BlockNumberOrTag::Latest) + .get_block_by_number(number) .await? .ok_or(anyhow::Error::msg("Blockchain has no blocks")) .map(|block| block.header.hash) @@ -491,12 +491,12 @@ impl EthereumNode for KitchensinkNode { } #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] - fn block_timestamp(&self) -> anyhow::Result { + fn block_timestamp(&self, number: BlockNumberOrTag) -> anyhow::Result { let provider = self.provider(); BlockingExecutor::execute(async move { provider .await? - .get_block_by_number(BlockNumberOrTag::Latest) + .get_block_by_number(number) .await? .ok_or(anyhow::Error::msg("Blockchain has no blocks")) .map(|block| block.header.timestamp) @@ -1256,7 +1256,7 @@ mod tests { let (node, _temp_dir) = new_node(); // Act - let gas_limit = node.block_gas_limit(); + let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest); // Assert let gas_limit = gas_limit.expect("Failed to get the gas limit"); @@ -1269,7 +1269,7 @@ mod tests { let (node, _temp_dir) = new_node(); // Act - let coinbase = node.block_coinbase(); + let coinbase = node.block_coinbase(BlockNumberOrTag::Latest); // Assert let coinbase = coinbase.expect("Failed to get the coinbase"); @@ -1282,7 +1282,7 @@ mod tests { let (node, _temp_dir) = new_node(); // Act - let block_difficulty = node.block_difficulty(); + let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest); // Assert let block_difficulty = block_difficulty.expect("Failed to get the block difficulty"); @@ -1295,7 +1295,7 @@ mod tests { let (node, _temp_dir) = new_node(); // Act - let block_hash = node.block_hash(); + let block_hash = node.block_hash(BlockNumberOrTag::Latest); // Assert let _ = block_hash.expect("Failed to get the block hash"); @@ -1307,7 +1307,7 @@ mod tests { let (node, _temp_dir) = new_node(); // Act - let block_timestamp = node.block_timestamp(); + let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest); // Assert let _ = block_timestamp.expect("Failed to get the block timestamp"); From 20da99784ea8e2d9f4dacfd7abdf1b8d86e4aa9e Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Mon, 14 Jul 2025 23:51:59 +0300 Subject: [PATCH 21/25] Add resolution logic for other matterlabs variables --- Cargo.lock | 1 + crates/core/src/driver/mod.rs | 38 ++--- crates/format/Cargo.toml | 2 + crates/format/src/input.rs | 260 ++++++++++++++++++++++++++++++---- 4 files changed, 258 insertions(+), 43 deletions(-) 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 ba7020d2..731bdfd7 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::{ @@ -135,17 +135,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:?}"); @@ -231,10 +235,12 @@ where // automatically fill in all of the missing fields from the provider that we // are using. let code = alloy::hex::decode(&code)?; - 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 270d908f..df868c2d 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 @@ -140,7 +144,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>()); } @@ -160,8 +164,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)), @@ -191,6 +197,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( @@ -212,30 +219,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))?) } } @@ -248,6 +265,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#" @@ -283,7 +363,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)); @@ -326,7 +406,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,); @@ -336,4 +418,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() + ) + ) + } } From 2537a132e6b0e146c4be43f09dabaa8a22b67674 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Tue, 15 Jul 2025 13:54:58 +0300 Subject: [PATCH 22/25] Fix tests --- crates/node/src/kitchensink.rs | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index d003f388..28726143 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -1039,23 +1039,30 @@ mod tests { (config, temp_dir) } - fn new_node() -> (KitchensinkNode, TempDir) { + 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. + 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, temp_dir) + (node, args, temp_dir) } #[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 = node.provider().await.expect("Failed to create provider"); @@ -1240,7 +1247,7 @@ mod tests { #[test] fn can_get_chain_id_from_node() { // Arrange - let (node, _temp_dir) = new_node(); + let (node, _args, _temp_dir) = new_node(); // Act let chain_id = node.chain_id(); @@ -1253,7 +1260,7 @@ mod tests { #[test] fn can_get_gas_limit_from_node() { // Arrange - let (node, _temp_dir) = new_node(); + let (node, _args, _temp_dir) = new_node(); // Act let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest); @@ -1266,7 +1273,7 @@ mod tests { #[test] fn can_get_coinbase_from_node() { // Arrange - let (node, _temp_dir) = new_node(); + let (node, _args, _temp_dir) = new_node(); // Act let coinbase = node.block_coinbase(BlockNumberOrTag::Latest); @@ -1279,7 +1286,7 @@ mod tests { #[test] fn can_get_block_difficulty_from_node() { // Arrange - let (node, _temp_dir) = new_node(); + let (node, _args, _temp_dir) = new_node(); // Act let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest); @@ -1292,7 +1299,7 @@ mod tests { #[test] fn can_get_block_hash_from_node() { // Arrange - let (node, _temp_dir) = new_node(); + let (node, _args, _temp_dir) = new_node(); // Act let block_hash = node.block_hash(BlockNumberOrTag::Latest); @@ -1304,7 +1311,7 @@ mod tests { #[test] fn can_get_block_timestamp_from_node() { // Arrange - let (node, _temp_dir) = new_node(); + let (node, _args, _temp_dir) = new_node(); // Act let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest); @@ -1316,7 +1323,7 @@ mod tests { #[test] fn can_get_block_number_from_node() { // Arrange - let (node, _temp_dir) = new_node(); + let (node, _args, _temp_dir) = new_node(); // Act let block_number = node.last_block_number(); From fa4bf950917b43c0253b6dd5926e24f1eb1b92a8 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Tue, 15 Jul 2025 13:57:31 +0300 Subject: [PATCH 23/25] Add comment on alternative solutions --- crates/node/src/kitchensink.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index 28726143..6b4689bc 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -1047,6 +1047,14 @@ mod tests { // 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(); From 5c64de7e674608dcd25892dce8c66d21915a1bf1 Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Tue, 15 Jul 2025 14:08:55 +0300 Subject: [PATCH 24/25] Change kitchensink gas limit assertion --- crates/node/src/kitchensink.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index 6b4689bc..68644cda 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -1274,8 +1274,7 @@ mod tests { 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, 52430300000000000000) + let _ = gas_limit.expect("Failed to get the gas limit"); } #[test] From 0d8caca14b600e10a2a33c155bda768f0ea6021d Mon Sep 17 00:00:00 2001 From: Omar Abdulla Date: Fri, 18 Jul 2025 15:06:02 +0300 Subject: [PATCH 25/25] Remove un-needed profile config --- Cargo.toml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a61ca4f5..9f3c8759 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,15 +78,3 @@ features = [ inherits = "release" lto = true 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"