From 491c23efb3f4bcce7daed38be98ac754f84924ca Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 14 Oct 2025 16:50:36 +0300 Subject: [PATCH] Remove the revive network (#188) * Remove the revive network * Add a provider method to the `EthereumNode` * Report the ref time and proof size for substrate chains in block information * Remove un-needed dependency --- Cargo.lock | 220 ++++++- Cargo.toml | 2 +- assets/revive_metadata.scale | Bin 0 -> 66887 bytes .../src/differential_benchmarks/watcher.rs | 12 +- crates/node-interaction/src/lib.rs | 17 + crates/node/Cargo.toml | 2 +- crates/node/src/node_implementations/geth.rs | 16 +- .../node_implementations/lighthouse_geth.rs | 21 +- .../src/node_implementations/substrate.rs | 559 +++--------------- .../src/node_implementations/zombienet.rs | 128 ++-- crates/node/src/provider_utils/provider.rs | 2 +- run_tests.sh | 9 +- 12 files changed, 438 insertions(+), 550 deletions(-) create mode 100644 assets/revive_metadata.scale diff --git a/Cargo.lock b/Cargo.lock index 77f248d..7bc7514 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2936,6 +2936,22 @@ dependencies = [ "sp-crypto-hashing", ] +[[package]] +name = "frame-decode" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c470df86cf28818dd3cd2fc4667b80dbefe2236c722c3dc1d09e7c6c82d6dfcd" +dependencies = [ + "frame-metadata", + "parity-scale-codec", + "scale-decode", + "scale-encode", + "scale-info", + "scale-type-resolver", + "sp-crypto-hashing", + "thiserror 2.0.12", +] + [[package]] name = "frame-metadata" version = "23.0.0" @@ -5668,7 +5684,6 @@ version = "0.1.0" dependencies = [ "alloy", "anyhow", - "async-stream", "futures", "revive-common", "revive-dt-common", @@ -5681,6 +5696,7 @@ dependencies = [ "serde_yaml_ng", "sp-core", "sp-runtime", + "subxt 0.44.0", "temp-dir", "tokio", "tower 0.5.2", @@ -7350,11 +7366,48 @@ dependencies = [ "serde", "serde_json", "sp-crypto-hashing", - "subxt-core", - "subxt-lightclient", - "subxt-macro", - "subxt-metadata", - "subxt-rpcs", + "subxt-core 0.43.0", + "subxt-lightclient 0.43.0", + "subxt-macro 0.43.0", + "subxt-metadata 0.43.0", + "subxt-rpcs 0.43.0", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "subxt" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddbf938ac1d86a361a84709a71cdbae5d87f370770b563651d1ec052eed9d0b4" +dependencies = [ + "async-trait", + "derive-where", + "either", + "frame-metadata", + "futures", + "hex", + "jsonrpsee", + "parity-scale-codec", + "primitive-types 0.13.1", + "scale-bits", + "scale-decode", + "scale-encode", + "scale-info", + "scale-value", + "serde", + "serde_json", + "sp-crypto-hashing", + "subxt-core 0.44.0", + "subxt-lightclient 0.44.0", + "subxt-macro 0.44.0", + "subxt-metadata 0.44.0", + "subxt-rpcs 0.44.0", "thiserror 2.0.12", "tokio", "tokio-util", @@ -7376,7 +7429,24 @@ dependencies = [ "quote", "scale-info", "scale-typegen", - "subxt-metadata", + "subxt-metadata 0.43.0", + "syn 2.0.101", + "thiserror 2.0.12", +] + +[[package]] +name = "subxt-codegen" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c250ad8cd102d40ae47977b03295a2ff791375f30ddc7474d399fb56efb793b" +dependencies = [ + "heck", + "parity-scale-codec", + "proc-macro2", + "quote", + "scale-info", + "scale-typegen", + "subxt-metadata 0.44.0", "syn 2.0.101", "thiserror 2.0.12", ] @@ -7390,7 +7460,7 @@ dependencies = [ "base58", "blake2", "derive-where", - "frame-decode", + "frame-decode 0.8.3", "frame-metadata", "hashbrown 0.14.5", "hex", @@ -7406,7 +7476,37 @@ dependencies = [ "serde", "serde_json", "sp-crypto-hashing", - "subxt-metadata", + "subxt-metadata 0.43.0", + "thiserror 2.0.12", + "tracing", +] + +[[package]] +name = "subxt-core" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5705c5b420294524e41349bf23c6b11aa474ce731de7317f4153390e1927f702" +dependencies = [ + "base58", + "blake2", + "derive-where", + "frame-decode 0.9.0", + "frame-metadata", + "hashbrown 0.14.5", + "hex", + "impl-serde", + "keccak-hash", + "parity-scale-codec", + "primitive-types 0.13.1", + "scale-bits", + "scale-decode", + "scale-encode", + "scale-info", + "scale-value", + "serde", + "serde_json", + "sp-crypto-hashing", + "subxt-metadata 0.44.0", "thiserror 2.0.12", "tracing", ] @@ -7428,6 +7528,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "subxt-lightclient" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e02732a6c9ae46bc282c1a741b3d3e494021b3e87e7e92cfb3620116d92911" +dependencies = [ + "futures", + "futures-util", + "serde", + "serde_json", + "smoldot-light", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tracing", +] + [[package]] name = "subxt-macro" version = "0.43.0" @@ -7439,9 +7556,26 @@ dependencies = [ "proc-macro-error2", "quote", "scale-typegen", - "subxt-codegen", - "subxt-metadata", - "subxt-utils-fetchmetadata", + "subxt-codegen 0.43.0", + "subxt-metadata 0.43.0", + "subxt-utils-fetchmetadata 0.43.0", + "syn 2.0.101", +] + +[[package]] +name = "subxt-macro" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501bf358698f5ab02a6199a1fcd3f1b482e2f5b6eb5d185411e6a74a175ec8e8" +dependencies = [ + "darling 0.20.11", + "parity-scale-codec", + "proc-macro-error2", + "quote", + "scale-typegen", + "subxt-codegen 0.44.0", + "subxt-metadata 0.44.0", + "subxt-utils-fetchmetadata 0.44.0", "syn 2.0.101", ] @@ -7451,7 +7585,22 @@ version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c134068711c0c46906abc0e6e4911204420331530738e18ca903a5469364d9f" dependencies = [ - "frame-decode", + "frame-decode 0.8.3", + "frame-metadata", + "hashbrown 0.14.5", + "parity-scale-codec", + "scale-info", + "sp-crypto-hashing", + "thiserror 2.0.12", +] + +[[package]] +name = "subxt-metadata" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fb7c0bfafad78dda7084c6a2444444744af3bbf7b2502399198b9b4c20eddf" +dependencies = [ + "frame-decode 0.9.0", "frame-metadata", "hashbrown 0.14.5", "parity-scale-codec", @@ -7476,8 +7625,32 @@ dependencies = [ "primitive-types 0.13.1", "serde", "serde_json", - "subxt-core", - "subxt-lightclient", + "subxt-core 0.43.0", + "subxt-lightclient 0.43.0", + "thiserror 2.0.12", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "subxt-rpcs" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab68a9c20ecedb0cb7d62d64f884e6add91bb70485783bf40aa8eac5c389c6e0" +dependencies = [ + "derive-where", + "frame-metadata", + "futures", + "hex", + "impl-serde", + "jsonrpsee", + "parity-scale-codec", + "primitive-types 0.13.1", + "serde", + "serde_json", + "subxt-core 0.44.0", + "subxt-lightclient 0.44.0", "thiserror 2.0.12", "tokio-util", "tracing", @@ -7507,7 +7680,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "sp-crypto-hashing", - "subxt-core", + "subxt-core 0.43.0", "thiserror 2.0.12", "zeroize", ] @@ -7523,6 +7696,17 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "subxt-utils-fetchmetadata" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f6812a653c5a3e63a079aa3b60a3f4c362722753c3222286eaa1800f9002" +dependencies = [ + "hex", + "parity-scale-codec", + "thiserror 2.0.12", +] + [[package]] name = "syn" version = "1.0.109" @@ -9316,7 +9500,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "sp-core", - "subxt", + "subxt 0.43.0", "subxt-signer", "thiserror 1.0.69", "tokio", @@ -9376,7 +9560,7 @@ dependencies = [ "async-trait", "futures", "lazy_static", - "subxt", + "subxt 0.43.0", "subxt-signer", "tokio", "zombienet-configuration", diff --git a/Cargo.toml b/Cargo.toml index d1c48b5..907844a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ revive-dt-solc-binaries = { version = "0.1.0", path = "crates/solc-binaries" } ansi_term = "0.12.1" anyhow = "1.0" -async-stream = { version = "0.3.6" } bson = { version = "2.15.0" } cacache = { version = "13.1.0" } clap = { version = "4", features = ["derive"] } @@ -51,6 +50,7 @@ sha2 = { version = "0.10.9" } sp-core = "36.1.0" sp-runtime = "41.1.0" strum = { version = "0.27.2", features = ["derive"] } +subxt = { version = "0.44.0" } temp-dir = { version = "0.1.16" } tempfile = "3.3" thiserror = "2" diff --git a/assets/revive_metadata.scale b/assets/revive_metadata.scale new file mode 100644 index 0000000000000000000000000000000000000000..68c0a1e5409b50af47bcf1af6a441f59bd486dc3 GIT binary patch literal 66887 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 b{8q9jz0bdn)e@2DR@=>RKhMkESGV{BCsoS| literal 0 HcmV?d00001 diff --git a/crates/core/src/differential_benchmarks/watcher.rs b/crates/core/src/differential_benchmarks/watcher.rs index 96f0098..473f134 100644 --- a/crates/core/src/differential_benchmarks/watcher.rs +++ b/crates/core/src/differential_benchmarks/watcher.rs @@ -106,7 +106,9 @@ impl Watcher { // region:TEMPORARY eprintln!("Watcher information for {}", self.platform_identifier); - eprintln!("block_number,block_timestamp,mined_gas,block_gas_limit,tx_count"); + eprintln!( + "block_number,block_timestamp,mined_gas,block_gas_limit,tx_count,ref_time,max_ref_time,proof_size,max_proof_size" + ); // endregion:TEMPORARY while let Some(block) = blocks_information_stream.next().await { // If the block number is equal to or less than the last block before the @@ -141,12 +143,16 @@ impl Watcher { // reporting in place and then it can be removed. This serves as as way of doing // some very simple reporting for the time being. eprintln!( - "\"{}\",\"{}\",\"{}\",\"{}\",\"{}\"", + "\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\"", block.block_number, block.block_timestamp, block.mined_gas, block.block_gas_limit, - block.transaction_hashes.len() + block.transaction_hashes.len(), + block.ref_time, + block.max_ref_time, + block.proof_size, + block.max_proof_size, ); // endregion:TEMPORARY diff --git a/crates/node-interaction/src/lib.rs b/crates/node-interaction/src/lib.rs index dbd2bf9..54796a6 100644 --- a/crates/node-interaction/src/lib.rs +++ b/crates/node-interaction/src/lib.rs @@ -3,7 +3,9 @@ use std::pin::Pin; use std::sync::Arc; +use alloy::network::Ethereum; use alloy::primitives::{Address, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256}; +use alloy::providers::DynProvider; use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace}; use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest}; use anyhow::Result; @@ -74,6 +76,9 @@ pub trait EthereumNode { + '_, >, >; + + fn provider(&self) + -> Pin>> + '_>>; } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -92,4 +97,16 @@ pub struct MinedBlockInformation { /// The hashes of the transactions that were mined as part of the block. pub transaction_hashes: Vec, + + /// The ref time for substrate based chains. + pub ref_time: u128, + + /// The max ref time for substrate based chains. + pub max_ref_time: u64, + + /// The proof size for substrate based chains. + pub proof_size: u128, + + /// The max proof size for substrate based chains. + pub max_proof_size: u64, } diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index dfe0827..cffca52 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -11,7 +11,6 @@ rust-version.workspace = true [dependencies] anyhow = { workspace = true } alloy = { workspace = true } -async-stream = { workspace = true } futures = { workspace = true } tracing = { workspace = true } tower = { workspace = true } @@ -30,6 +29,7 @@ serde_yaml_ng = { workspace = true } sp-core = { workspace = true } sp-runtime = { workspace = true } +subxt = { workspace = true } zombienet-sdk = { workspace = true } [dev-dependencies] diff --git a/crates/node/src/node_implementations/geth.rs b/crates/node/src/node_implementations/geth.rs index fc31d86..097265d 100644 --- a/crates/node/src/node_implementations/geth.rs +++ b/crates/node/src/node_implementations/geth.rs @@ -32,7 +32,7 @@ use alloy::{ }, }; use anyhow::Context as _; -use futures::{Stream, StreamExt}; +use futures::{FutureExt, Stream, StreamExt}; use revive_common::EVMVersion; use tokio::sync::OnceCell; use tracing::{Instrument, error, instrument}; @@ -535,6 +535,10 @@ impl EthereumNode for GethNode { .as_hashes() .expect("Must be hashes") .to_vec(), + ref_time: 0, + max_ref_time: 0, + proof_size: 0, + max_proof_size: 0, }) }); @@ -542,6 +546,16 @@ impl EthereumNode for GethNode { as Pin>>) }) } + + fn provider( + &self, + ) -> Pin>> + '_>> + { + Box::pin( + self.provider() + .map(|provider| provider.map(|provider| provider.erased())), + ) + } } pub struct GethNodeResolver { diff --git a/crates/node/src/node_implementations/lighthouse_geth.rs b/crates/node/src/node_implementations/lighthouse_geth.rs index f119107..beca317 100644 --- a/crates/node/src/node_implementations/lighthouse_geth.rs +++ b/crates/node/src/node_implementations/lighthouse_geth.rs @@ -43,7 +43,7 @@ use alloy::{ }, }; use anyhow::Context as _; -use futures::{Stream, StreamExt}; +use futures::{FutureExt, Stream, StreamExt}; use revive_common::EVMVersion; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_with::serde_as; @@ -222,6 +222,7 @@ impl LighthouseGethNode { "--ws.port=8546".to_string(), "--ws.api=eth,net,web3,txpool,engine".to_string(), "--ws.origins=*".to_string(), + "--miner.gaslimit=30000000".to_string(), ], consensus_layer_extra_parameters: vec![ "--disable-quic".to_string(), @@ -247,6 +248,8 @@ impl LighthouseGethNode { .collect::>(); serde_json::to_string(&map).unwrap() }, + gas_limit: 30_000_000, + genesis_gaslimit: 30_000_000, }, wait_for_finalization: false, port_publisher: Some(PortPublisherParameters { @@ -754,6 +757,10 @@ impl EthereumNode for LighthouseGethNode { .as_hashes() .expect("Must be hashes") .to_vec(), + ref_time: 0, + max_ref_time: 0, + proof_size: 0, + max_proof_size: 0, }) }); @@ -761,6 +768,16 @@ impl EthereumNode for LighthouseGethNode { as Pin>>) }) } + + fn provider( + &self, + ) -> Pin>> + '_>> + { + Box::pin( + self.http_provider() + .map(|provider| provider.map(|provider| provider.erased())), + ) + } } pub struct LighthouseGethNodeResolver, P: Provider> { @@ -1035,6 +1052,8 @@ struct NetworkParameters { pub num_validator_keys_per_node: u64, pub genesis_delay: u64, + pub genesis_gaslimit: u64, + pub gas_limit: u64, pub prefunded_accounts: String, } diff --git a/crates/node/src/node_implementations/substrate.rs b/crates/node/src/node_implementations/substrate.rs index ccf1e75..e4fdec4 100644 --- a/crates/node/src/node_implementations/substrate.rs +++ b/crates/node/src/node_implementations/substrate.rs @@ -11,17 +11,10 @@ use std::{ }; use alloy::{ - consensus::{BlockHeader, TxEnvelope}, eips::BlockNumberOrTag, genesis::{Genesis, GenesisAccount}, - network::{ - Ethereum, EthereumWallet, Network, NetworkWallet, TransactionBuilder, - TransactionBuilderError, UnbuiltTransactionError, - }, - primitives::{ - Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, StorageKey, - TxHash, U256, - }, + network::{Ethereum, EthereumWallet, NetworkWallet}, + primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256}, providers::{ Provider, ext::DebugApi, @@ -29,25 +22,23 @@ use alloy::{ }, rpc::types::{ EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest, - eth::{Block, Header, Transaction}, trace::geth::{ DiffMode, GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateFrame, }, }, }; use anyhow::Context as _; -use async_stream::stream; -use futures::Stream; +use futures::{FutureExt, Stream, StreamExt}; use revive_common::EVMVersion; use revive_dt_common::fs::clear_directory; use revive_dt_format::traits::ResolverApi; -use serde::{Deserialize, Serialize}; use serde_json::{Value as JsonValue, json}; use sp_core::crypto::Ss58Codec; use sp_runtime::AccountId32; use revive_dt_config::*; use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation}; +use subxt::{OnlineClient, SubstrateConfig}; use tokio::sync::OnceCell; use tracing::instrument; @@ -80,7 +71,7 @@ pub struct SubstrateNode { eth_proxy_process: Option, wallet: Arc, nonce_manager: CachedNonceManager, - provider: OnceCell>>, + provider: OnceCell>>, consensus: Option, } @@ -353,12 +344,10 @@ impl SubstrateNode { Ok(String::from_utf8_lossy(&output).trim().to_string()) } - async fn provider( - &self, - ) -> anyhow::Result>> { + async fn provider(&self) -> anyhow::Result>> { self.provider .get_or_try_init(|| async move { - construct_concurrency_limited_provider::( + construct_concurrency_limited_provider::( self.rpc_url.as_str(), FallbackGasFiller::new(u64::MAX, 5_000_000_000, 1_000_000_000), ChainIdFiller::new(Some(CHAIN_ID)), @@ -519,53 +508,97 @@ impl EthereumNode for SubstrateNode { + '_, >, > { - fn create_stream( - provider: ConcreteProvider>, - ) -> impl Stream { - stream! { - let mut block_number = provider.get_block_number().await.expect("Failed to get the block number"); - loop { - tokio::time::sleep(Duration::from_secs(1)).await; + #[subxt::subxt(runtime_metadata_path = "../../assets/revive_metadata.scale")] + pub mod revive {} - let Ok(Some(block)) = provider.get_block_by_number(BlockNumberOrTag::Number(block_number)).await - else { - continue; - }; + Box::pin(async move { + let substrate_rpc_port = Self::BASE_SUBSTRATE_RPC_PORT + self.id as u16; + let substrate_rpc_url = format!("ws://127.0.0.1:{substrate_rpc_port}"); + let api = OnlineClient::::from_url(substrate_rpc_url) + .await + .context("Failed to create subxt rpc client")?; + let provider = self.provider().await.context("Failed to create provider")?; - block_number += 1; - yield MinedBlockInformation { - block_number: block.number(), - block_timestamp: block.header.timestamp, - mined_gas: block.header.gas_used as _, - block_gas_limit: block.header.gas_limit, - transaction_hashes: block + let block_stream = api + .blocks() + .subscribe_all() + .await + .context("Failed to subscribe to blocks")?; + + let mined_block_information_stream = block_stream.filter_map(move |block| { + let api = api.clone(); + let provider = provider.clone(); + + async move { + let substrate_block = block.ok()?; + let revive_block = provider + .get_block_by_number( + BlockNumberOrTag::Number(substrate_block.number() as _), + ) + .await + .expect("TODO: Remove") + .expect("TODO: Remove"); + + let used = api + .storage() + .at(substrate_block.reference()) + .fetch_or_default(&revive::storage().system().block_weight()) + .await + .expect("TODO: Remove"); + + let block_ref_time = (used.normal.ref_time as u128) + + (used.operational.ref_time as u128) + + (used.mandatory.ref_time as u128); + let block_proof_size = (used.normal.proof_size as u128) + + (used.operational.proof_size as u128) + + (used.mandatory.proof_size as u128); + + let limits = api + .constants() + .at(&revive::constants().system().block_weights()) + .expect("TODO: Remove"); + + let max_ref_time = limits.max_block.ref_time; + let max_proof_size = limits.max_block.proof_size; + + Some(MinedBlockInformation { + block_number: substrate_block.number() as _, + block_timestamp: revive_block.header.timestamp, + mined_gas: revive_block.header.gas_used as _, + block_gas_limit: revive_block.header.gas_limit as _, + transaction_hashes: revive_block .transactions .into_hashes() .as_hashes() .expect("Must be hashes") .to_vec(), - }; - }; - } - } + ref_time: block_ref_time, + max_ref_time, + proof_size: block_proof_size, + max_proof_size, + }) + } + }); - Box::pin(async move { - let provider = self - .provider() - .await - .context("Failed to create the provider for a block subscription")?; - - let stream = Box::pin(create_stream(provider)) - as Pin>>; - - Ok(stream) + Ok(Box::pin(mined_block_information_stream) + as Pin>>) }) } + + fn provider( + &self, + ) -> Pin>> + '_>> + { + Box::pin( + self.provider() + .map(|provider| provider.map(|provider| provider.erased())), + ) + } } pub struct SubstrateNodeResolver { id: u32, - provider: ConcreteProvider>, + provider: ConcreteProvider>, } impl ResolverApi for SubstrateNodeResolver { @@ -729,430 +762,6 @@ impl Drop for SubstrateNode { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ReviveNetwork; - -impl Network for ReviveNetwork { - type TxType = ::TxType; - - type TxEnvelope = ::TxEnvelope; - - type UnsignedTx = ::UnsignedTx; - - type ReceiptEnvelope = ::ReceiptEnvelope; - - type Header = ReviveHeader; - - 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 take_nonce(&mut self) -> Option { - <::TransactionRequest as TransactionBuilder>::take_nonce( - self, - ) - } - - 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, ReviveNetwork> { - 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 ReviveHeader { - /// 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 ReviveHeader { - 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 alloy::rpc::types::TransactionRequest; diff --git a/crates/node/src/node_implementations/zombienet.rs b/crates/node/src/node_implementations/zombienet.rs index a5ac8d1..8387685 100644 --- a/crates/node/src/node_implementations/zombienet.rs +++ b/crates/node/src/node_implementations/zombienet.rs @@ -55,8 +55,7 @@ use alloy::{ }; use anyhow::Context as _; -use async_stream::stream; -use futures::Stream; +use futures::{FutureExt, Stream, StreamExt}; use revive_common::EVMVersion; use revive_dt_common::fs::clear_directory; use revive_dt_config::*; @@ -65,6 +64,7 @@ use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation}; use serde_json::{Value as JsonValue, json}; use sp_core::crypto::Ss58Codec; use sp_runtime::AccountId32; +use subxt::{OnlineClient, SubstrateConfig}; use tokio::sync::OnceCell; use tracing::instrument; use zombienet_sdk::{LocalFileSystem, NetworkConfigBuilder, NetworkConfigExt}; @@ -73,7 +73,6 @@ use crate::{ Node, constants::INITIAL_BALANCE, helpers::{Process, ProcessReadinessWaitBehavior}, - node_implementations::substrate::ReviveNetwork, provider_utils::{ ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider, execute_transaction, @@ -111,7 +110,7 @@ pub struct ZombienetNode { wallet: Arc, nonce_manager: CachedNonceManager, - provider: OnceCell>>, + provider: OnceCell>>, } impl ZombienetNode { @@ -399,12 +398,10 @@ impl ZombienetNode { Ok(String::from_utf8_lossy(&output).trim().to_string()) } - async fn provider( - &self, - ) -> anyhow::Result>> { + async fn provider(&self) -> anyhow::Result>> { self.provider .get_or_try_init(|| async move { - construct_concurrency_limited_provider::( + construct_concurrency_limited_provider::( self.connection_string.as_str(), FallbackGasFiller::new(u64::MAX, 5_000_000_000, 1_000_000_000), ChainIdFiller::default(), // TODO: use CHAIN_ID constant @@ -567,58 +564,99 @@ impl EthereumNode for ZombienetNode { + '_, >, > { - fn create_stream( - provider: ConcreteProvider>, - ) -> impl Stream { - stream! { - let mut block_number = provider.get_block_number().await.expect("Failed to get the block number"); - loop { - tokio::time::sleep(Duration::from_secs(1)).await; + #[subxt::subxt(runtime_metadata_path = "../../assets/revive_metadata.scale")] + pub mod revive {} - let Ok(Some(block)) = provider.get_block_by_number(BlockNumberOrTag::Number(block_number)).await - else { - continue; - }; + Box::pin(async move { + let substrate_rpc_url = format!("ws://127.0.0.1:{}", self.node_rpc_port.unwrap()); + let api = OnlineClient::::from_url(substrate_rpc_url) + .await + .context("Failed to create subxt rpc client")?; + let provider = self.provider().await.context("Failed to create provider")?; - block_number += 1; - yield MinedBlockInformation { - block_number: block.number(), - block_timestamp: block.header.timestamp, - mined_gas: block.header.gas_used as _, - block_gas_limit: block.header.gas_limit, - transaction_hashes: block + let block_stream = api + .blocks() + .subscribe_all() + .await + .context("Failed to subscribe to blocks")?; + + let mined_block_information_stream = block_stream.filter_map(move |block| { + let api = api.clone(); + let provider = provider.clone(); + + async move { + let substrate_block = block.ok()?; + let revive_block = provider + .get_block_by_number( + BlockNumberOrTag::Number(substrate_block.number() as _), + ) + .await + .expect("TODO: Remove") + .expect("TODO: Remove"); + + let used = api + .storage() + .at(substrate_block.reference()) + .fetch_or_default(&revive::storage().system().block_weight()) + .await + .expect("TODO: Remove"); + + let block_ref_time = (used.normal.ref_time as u128) + + (used.operational.ref_time as u128) + + (used.mandatory.ref_time as u128); + let block_proof_size = (used.normal.proof_size as u128) + + (used.operational.proof_size as u128) + + (used.mandatory.proof_size as u128); + + let limits = api + .constants() + .at(&revive::constants().system().block_weights()) + .expect("TODO: Remove"); + + let max_ref_time = limits.max_block.ref_time; + let max_proof_size = limits.max_block.proof_size; + + Some(MinedBlockInformation { + block_number: substrate_block.number() as _, + block_timestamp: revive_block.header.timestamp, + mined_gas: revive_block.header.gas_used as _, + block_gas_limit: revive_block.header.gas_limit as _, + transaction_hashes: revive_block .transactions .into_hashes() .as_hashes() .expect("Must be hashes") .to_vec(), - }; - }; - } - } + ref_time: block_ref_time, + max_ref_time, + proof_size: block_proof_size, + max_proof_size, + }) + } + }); - Box::pin(async move { - let provider = self - .provider() - .await - .context("Failed to create the provider for a block subscription")?; - - let stream = Box::pin(create_stream(provider)) - as Pin>>; - - Ok(stream) + Ok(Box::pin(mined_block_information_stream) + as Pin>>) }) } + + fn provider( + &self, + ) -> Pin>> + '_>> + { + Box::pin( + self.provider() + .map(|provider| provider.map(|provider| provider.erased())), + ) + } } -pub struct ZombieNodeResolver, P: Provider> { +pub struct ZombieNodeResolver, P: Provider> { id: u32, - provider: FillProvider, + provider: FillProvider, } -impl, P: Provider> ResolverApi - for ZombieNodeResolver -{ +impl, P: Provider> ResolverApi for ZombieNodeResolver { #[instrument(level = "info", skip_all, fields(zombie_node_id = self.id))] fn chain_id( &self, diff --git a/crates/node/src/provider_utils/provider.rs b/crates/node/src/provider_utils/provider.rs index f10b3b6..ba7a2c5 100644 --- a/crates/node/src/provider_utils/provider.rs +++ b/crates/node/src/provider_utils/provider.rs @@ -44,7 +44,7 @@ where // requests at any point of time and no more than that. This is done in an effort to stabilize // the framework from some of the interment issues that we've been seeing related to RPC calls. static GLOBAL_CONCURRENCY_LIMITER_LAYER: LazyLock = - LazyLock::new(|| ConcurrencyLimiterLayer::new(500)); + LazyLock::new(|| ConcurrencyLimiterLayer::new(1000)); let client = ClientBuilder::default() .layer(GLOBAL_CONCURRENCY_LIMITER_LAYER.clone()) diff --git a/run_tests.sh b/run_tests.sh index 37817c6..c0bd229 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -76,7 +76,7 @@ cat > "$CORPUS_FILE" << EOF { "name": "MatterLabs Solidity Simple, Complex, and Semantic Tests", "paths": [ - "$(realpath "$TEST_REPO_DIR/fixtures/solidity/simple")" + "$(realpath "$TEST_REPO_DIR/fixtures/solidity")" ] } EOF @@ -93,16 +93,17 @@ echo "" # Run the tool cargo build --release; RUST_LOG="info,alloy_pubsub::service=error" ./target/release/retester test \ - --platform geth-evm-solc \ + --platform revive-dev-node-polkavm-resolc \ --corpus "$CORPUS_FILE" \ --working-directory "$WORKDIR" \ --concurrency.number-of-nodes 10 \ --concurrency.number-of-threads 5 \ - --concurrency.ignore-concurrency-limit \ + --concurrency.number-of-concurrent-tasks 500 \ --wallet.additional-keys 100000 \ --kitchensink.path "$SUBSTRATE_NODE_BIN" \ --revive-dev-node.path "$REVIVE_DEV_NODE_BIN" \ --eth-rpc.path "$ETH_RPC_BIN" \ - > logs.log + > logs.log \ + 2> output.log echo -e "${GREEN}=== Test run completed! ===${NC}" \ No newline at end of file