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..27c2f207 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,9 +67,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "1.0.9" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0093d23bf026b580c1f66ed3a053d8209c104a446c5264d3ad99587f6edef24e" +checksum = "ae58d888221eecf621595e2096836ce7cfc37be06bfa39d7f64aa6a3ea4c9e5b" dependencies = [ "alloy-consensus", "alloy-contract", @@ -162,9 +162,9 @@ dependencies = [ [[package]] name = "alloy-core" -version = "1.1.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3c5a28f166629752f2e7246b813cdea3243cca59aab2d4264b1fd68392c10eb" +checksum = "ad31216895d27d307369daa1393f5850b50bbbd372478a9fa951c095c210627e" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -175,9 +175,9 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "1.1.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18cc14d832bc3331ca22a1c7819de1ede99f58f61a7d123952af7dde8de124a6" +checksum = "7b95b3deca680efc7e9cba781f1a1db352fa1ea50e6384a514944dcf4419e652" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -3998,10 +3998,12 @@ dependencies = [ "anyhow", "revive-dt-config", "revive-dt-node-interaction", + "serde", "serde_json", "sp-core", "sp-runtime", "temp-dir", + "tokio", "tracing", ] 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/config/src/lib.rs b/crates/config/src/lib.rs index e72e8f65..7f1c92ed 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1,4 +1,4 @@ -//! The global configuration used accross all revive differential testing crates. +//! The global configuration used across all revive differential testing crates. use std::{ fmt::Display, diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 1a9dcb0a..47b6d58f 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -12,10 +12,12 @@ rust-version.workspace = true anyhow = { workspace = true } alloy = { workspace = true } tracing = { workspace = true } +tokio = { 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/geth.rs b/crates/node/src/geth.rs index c40b18fb..066b3769 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -159,17 +159,85 @@ impl EthereumNode for Instance { let connection_string = self.connection_string(); let wallet = self.wallet.clone(); - tracing::debug!("Submitting transaction: {transaction:#?}"); - execute_transaction(Box::pin(async move { - Ok(ProviderBuilder::new() + 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? - .send_transaction(transaction) - .await? - .get_receipt() - .await?) + .await?; + + let pending_transaction = provider.send_transaction(transaction).await?; + let transaction_hash = pending_transaction.tx_hash(); + + let span = tracing::info_span!("Awaiting transaction receipt", ?transaction_hash); + let _guard = span.enter(); + + // The following is a fix for the "transaction indexing is in progress" error that we + // used to get. You can find more information on this in the following GH issue in geth + // https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on, + // before we can get the receipt of the transaction it needs to have been indexed by the + // node's indexer. Just because the transaction has been confirmed it doesn't mean that + // it has been indexed. When we call alloy's `get_receipt` it checks if the transaction + // was confirmed. If it has been, then it will call `eth_getTransactionReceipt` method + // which _might_ return the above error if the tx has not yet been indexed yet. So, we + // need to implement a retry mechanism for the receipt to keep retrying to get it until + // it eventually works, but we only do that if the error we get back is the "transaction + // indexing is in progress" error or if the receipt is None. + // + // At the moment we do not allow for the 60 seconds to be modified and we take it as + // being an implementation detail that's invisible to anything outside of this module. + // + // We allow a total of 60 retries for getting the receipt with one second between each + // retry and the next which means that we allow for a total of 60 seconds of waiting + // before we consider that we're unable to get the transaction receipt. + let mut retries = 0; + loop { + match provider.get_transaction_receipt(*transaction_hash).await { + Ok(Some(receipt)) => { + tracing::info!("Obtained the transaction receipt"); + break Ok(receipt); + } + Ok(None) => { + if retries == 60 { + tracing::error!( + "Polled for transaction receipt for 60 seconds but failed to get it" + ); + break Err(anyhow::anyhow!("Failed to get the transaction receipt")); + } else { + tracing::trace!( + retries, + "Sleeping for 1 second and trying to get the receipt again" + ); + retries += 1; + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + continue; + } + } + Err(error) => { + let error_string = error.to_string(); + if error_string.contains("transaction indexing is in progress") { + if retries == 60 { + tracing::error!( + "Polled for transaction receipt for 60 seconds but failed to get it" + ); + break Err(error.into()); + } else { + tracing::trace!( + retries, + "Sleeping for 1 second and trying to get the receipt again" + ); + retries += 1; + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + continue; + } + } else { + break Err(error.into()); + } + } + } + } })) } @@ -270,6 +338,7 @@ impl Node for Instance { impl Drop for Instance { fn drop(&mut self) { + tracing::info!(id = self.id, "Dropping node"); if let Some(child) = self.handle.as_mut() { let _ = child.kill(); } diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index 4f14a18b..239fe13c 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -12,15 +12,21 @@ use std::{ }; use alloy::{ + consensus::{BlockHeader, TxEnvelope}, hex, - network::EthereumWallet, - primitives::Address, + network::{ + Ethereum, EthereumWallet, Network, TransactionBuilder, TransactionBuilderError, + UnbuiltTransactionError, + }, + 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 sp_core::crypto::Ss58Codec; use sp_runtime::AccountId32; @@ -254,8 +260,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 +271,9 @@ impl EthereumNode for KitchensinkNode { .await? .get_receipt() .await?) - })) + })); + tracing::info!(?receipt, "Submitted tx to kitchensink"); + receipt } fn trace_transaction( @@ -281,6 +291,7 @@ impl EthereumNode for KitchensinkNode { trace_transaction(Box::pin(async move { Ok(ProviderBuilder::new() + .network::() .wallet(wallet) .connect(&url) .await? @@ -374,6 +385,427 @@ 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, + 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, 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;