From 04d56da477559fd36179bcc6c2442748c0f45d89 Mon Sep 17 00:00:00 2001 From: Marios Christou Date: Tue, 23 Sep 2025 17:32:57 +0300 Subject: [PATCH] [WIP] - Impl EthereumNode for zombie node --- crates/common/src/types/identifiers.rs | 4 + crates/config/src/lib.rs | 49 ++++++ crates/node/src/zombie.rs | 215 ++++++++++++++++++++++++- 3 files changed, 260 insertions(+), 8 deletions(-) diff --git a/crates/common/src/types/identifiers.rs b/crates/common/src/types/identifiers.rs index 349081c..7bb9024 100644 --- a/crates/common/src/types/identifiers.rs +++ b/crates/common/src/types/identifiers.rs @@ -39,6 +39,10 @@ pub enum PlatformIdentifier { ReviveDevNodePolkavmResolc, /// The revive dev node with the REVM backend with the solc compiler. ReviveDevNodeRevmSolc, + // /// A zombienet based Substrate/Polkadot node with the PolkaVM backend with the resolc compiler. + // ZombieNetPolkavmResolc, + // /// A zombienet based Substrate/Polkadot node with the REVM backend with the solc compiler. + // ZombieNetRevmSolc, } /// An enum of the platform identifiers of all of the platforms supported by this framework. diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index b1551bc..867e2d9 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -88,6 +88,15 @@ impl AsRef for Context { } } +impl AsRef for Context { + fn as_ref(&self) -> &ZombieNetConfiguration { + match self { + Self::ExecuteTests(context) => context.as_ref().as_ref(), + Self::ExportJsonSchema => unreachable!(), + } + } +} + impl AsRef for Context { fn as_ref(&self) -> &KitchensinkConfiguration { match self { @@ -195,6 +204,10 @@ pub struct TestExecutionContext { #[clap(flatten, next_help_heading = "Resolc Configuration")] pub resolc_configuration: ResolcConfiguration, + /// Configuration parameters for the Zombienet. + #[clap(flatten, next_help_heading = "Zombienet Configuration")] + pub zombienet_configuration: ZombieNetConfiguration, + /// Configuration parameters for the geth node. #[clap(flatten, next_help_heading = "Geth Configuration")] pub geth_configuration: GethConfiguration, @@ -266,6 +279,12 @@ impl AsRef for TestExecutionContext { } } +impl AsRef for TestExecutionContext { + fn as_ref(&self) -> &ZombieNetConfiguration { + &self.zombienet_configuration + } +} + impl AsRef for TestExecutionContext { fn as_ref(&self) -> &KurtosisConfiguration { &self.lighthouse_configuration @@ -340,6 +359,34 @@ pub struct ResolcConfiguration { pub path: PathBuf, } +/// A set of configuration parameters for Zombienet. +#[derive(Clone, Debug, Parser, Serialize)] +pub struct ZombieNetConfiguration { + /// Specifies the path of the zombienet node to be used by the tool. + /// + /// If this is not specified, then the tool assumes that it should use the zombienet binary + /// that's provided in the user's $PATH. + #[clap( + id = "zombienet.path", + long = "zombienet.path", + default_value = "polkadot-parachain" + )] + pub path: PathBuf, + + /// The amount of time to wait upon startup before considering that the node timed out. + #[clap( + id = "zombienet.start-timeout-ms", + long = "zombienet.start-timeout-ms", + default_value = "5000", + value_parser = parse_duration + )] + pub start_timeout_ms: Duration, + + /// This configures the tool to use Zombienet instead of using the revive-dev-node. + #[clap(long = "zombienet.dont-use-dev-node")] + pub use_zombienet: bool, +} + /// A set of configuration parameters for Geth. #[derive(Clone, Debug, Parser, Serialize)] pub struct GethConfiguration { @@ -692,4 +739,6 @@ pub enum TestingPlatform { Geth, /// The kitchensink runtime provides the PolkaVM (PVM) based node implementation. Kitchensink, + /// A polkadot/Substrate based network + ZombieNet, } diff --git a/crates/node/src/zombie.rs b/crates/node/src/zombie.rs index cad6dd5..ff14eea 100644 --- a/crates/node/src/zombie.rs +++ b/crates/node/src/zombie.rs @@ -16,7 +16,7 @@ use alloy::{ eips::BlockNumberOrTag, genesis::{Genesis, GenesisAccount}, network::{ - Ethereum, EthereumWallet, Network, NetworkWallet, TransactionBuilder, + self, Ethereum, EthereumWallet, Network, NetworkWallet, TransactionBuilder, TransactionBuilderError, UnbuiltTransactionError, }, primitives::{ @@ -46,8 +46,14 @@ use sp_runtime::AccountId32; use revive_dt_config::*; use revive_dt_node_interaction::EthereumNode; use tracing::instrument; +use zombienet_sdk::{ + LocalFileSystem, NetworkConfigBuilder, NetworkConfigExt, Orchestrator, OrchestratorError, + subxt::{client::OnlineClient, config, tx::TxClient}, +}; -use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE}; +use crate::{ + Node, common::FallbackGasFiller, constants::INITIAL_BALANCE, substrate::ReviveNetwork, +}; static NODE_COUNT: AtomicU32 = AtomicU32::new(0); @@ -64,6 +70,8 @@ pub struct ZombieNode { nonce_manager: CachedNonceManager, chain_id_filler: ChainIdFiller, logs_file_to_flush: Vec, + network_config: Option, + network: Option>, } impl ZombieNode { @@ -71,6 +79,11 @@ impl ZombieNode { const DATA_DIRECTORY: &str = "data"; const LOGS_DIRECTORY: &str = "logs"; + const CHAIN_SPEC_JSON_FILE: &str = "template_chainspec.json"; + + const BASE_RPC_PORT: u16 = 9944; + const PARACHAIN_ID: u32 = 100; + const ZOMBIENET_STDOUT_LOG_FILE_NAME: &str = "node_stdout.log"; const ZOMBIENET_STDERR_LOG_FILE_NAME: &str = "node_stderr.log"; @@ -81,7 +94,10 @@ impl ZombieNode { ) -> Self { let working_directory_path = AsRef::::as_ref(&context); let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst); - let base_directory = working_directory_path.as_path().join(Self::BASE_DIRECTORY); + let base_directory = working_directory_path + .as_path() + .join(Self::BASE_DIRECTORY) + .join(id.to_string()); let logs_directory = base_directory.join(Self::LOGS_DIRECTORY); let wallet = AsRef::::as_ref(&context).wallet(); @@ -96,11 +112,94 @@ impl ZombieNode { } fn init(&mut self, mut genesis: Genesis) -> anyhow::Result<&mut Self> { - todo!() + let _ = clear_directory(&self.base_directory); + let _ = clear_directory(&self.logs_directory); + + create_dir_all(&self.base_directory) + .context("Failed to create base directory for zombie node")?; + create_dir_all(&self.logs_directory) + .context("Failed to create logs directory for zombie node")?; + + let _genesis = serde_json::to_value(genesis)?; // TODO: validate this + + let network_config = NetworkConfigBuilder::new() + .with_relaychain(|r| { + r.with_chain("rococo-local") + .with_default_command("polkadot") + //.with_genesis_overrides(zombie_genesis) + //.with_chain_spec_path(self.base_directory.join(Self::CHAIN_SPEC_JSON_FILE)) + .with_node(|node| node.with_name("alice")) + }) + .with_global_settings(|g| g.with_base_dir(&self.base_directory)) + .with_parachain(|p| { + p.with_id(Self::PARACHAIN_ID) + .evm_based(true) + .with_collator(|n| { + n.with_name("collator") + .with_command("polkadot-parachain") + //.with_args(vec!["--rpc-methods=Unsafe".into(), "--rpc-cors=all".into()]) + .with_ws_port(Self::BASE_RPC_PORT + self.id as u16) + }) + }) + .build() + .map_err(|e| anyhow::anyhow!("Failed to build zombienet network config: {e:?}"))?; + + self.network_config = Some(network_config); + + Ok(self) } - fn spawn_process(&mut self) -> anyhow::Result<()> { - todo!() + async fn spawn_process(&mut self) -> anyhow::Result<&mut Self> { + let Some(network_config) = self.network_config.clone() else { + anyhow::bail!("Node not initialized, call init() first"); + }; + + let network = network_config + .spawn_native() + .await + .context("Failed to spawn zombienet network")?; + network + .wait_until_is_up(60) + .await + .context("Network failed to start within timeout")?; + + let ws_uri = network + .parachain(Self::PARACHAIN_ID) + .context("Failed to get parachain from zombienet network")? + .collators() + .first() + .context("No collators found in zombienet parachain")? + .ws_uri(); + let ws_uri = ws_uri.replace("ws", "http"); + self.connection_string = ws_uri.to_string(); + self.network = Some(network); + + Ok(self) + } + + async fn provider( + &self, + ) -> anyhow::Result< + FillProvider, impl Provider, ReviveNetwork>, + > { + let Some(_network) = &self.network else { + anyhow::bail!("Node not initialized, call spawn() first"); + }; + + Ok(ProviderBuilder::new() + .disable_recommended_fillers() + .network::() + .filler(FallbackGasFiller::new( + 25_000_000, + 1_000_000_000, + 1_000_000_000, + )) + .filler(self.chain_id_filler.clone()) + .filler(NonceFiller::new(self.nonce_manager.clone())) + .wallet(self.wallet.clone()) + .connect(&self.connection_string) + .await + .context("Failed to connect to parachain Ethereum RPC")?) } } @@ -117,7 +216,19 @@ impl EthereumNode for ZombieNode { &self, transaction: alloy::rpc::types::TransactionRequest, ) -> Pin> + '_>> { - todo!() + Box::pin(async move { + let receipt = self + .provider() + .await + .context("Failed to create provider for transaction submission")? + .send_transaction(transaction) + .await + .context("Failed to submit transaction to substrate proxy")? + .get_receipt() + .await + .context("Failed to fetch transaction receipt from substrate proxy")?; + Ok(receipt) + }) } fn trace_transaction( @@ -126,7 +237,14 @@ impl EthereumNode for ZombieNode { trace_options: GethDebugTracingOptions, ) -> Pin> + '_>> { - todo!() + Box::pin(async move { + self.provider() + .await + .context("Failed to create provider for debug tracing")? + .debug_trace_transaction(tx_hash, trace_options) + .await + .context("Failed to obtain debug trace from substrate proxy") + }) } fn state_diff( @@ -161,3 +279,84 @@ impl EthereumNode for ZombieNode { todo!() } } + +#[cfg(test)] +mod tests { + use alloy::rpc::types::TransactionRequest; + use std::sync::{LazyLock, Mutex}; + + use std::fs; + + use super::*; + use crate::Node; + + fn test_config() -> TestExecutionContext { + let mut context = TestExecutionContext::default(); + context.zombienet_configuration.use_zombienet = true; + context + } + + fn new_node() -> (TestExecutionContext, ZombieNode) { + let context = test_config(); + let mut node = ZombieNode::new(&context); + let genesis = context.genesis_configuration.genesis().unwrap().clone(); + node.init(genesis).unwrap(); + (context, node) + } + + #[test] + fn zombie_node_id_is_unique_and_incremental() { + let context = test_config(); + let mut ids = Vec::new(); + for _ in 0..5 { + let node = ZombieNode::new(&context); + ids.push(node.id); + } + // Check uniqueness + let mut sorted = ids.clone(); + sorted.sort_unstable(); + sorted.dedup(); + assert_eq!(sorted.len(), ids.len(), "Node ids should be unique"); + // Check strictly increasing + for w in ids.windows(2) { + assert!(w[1] > w[0], "Node ids should be strictly increasing"); + } + } + + #[test] + fn zombie_node_spawn() { + let (context, mut node) = new_node(); + let genesis = context.genesis_configuration.genesis().unwrap().clone(); + let network = node.init(genesis).unwrap(); + + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { network.spawn_process().await }); + + assert!(result.is_ok(), "Zombienet should spawn successfully"); + } + + #[tokio::test] + async fn test_transfer_transaction_should_return_receipt() { + let (context, mut node) = new_node(); + + let node = node.spawn_process().await.unwrap(); + + let provider = node.provider().await.expect("Failed to create provider"); + let account_address = context + .wallet_configuration + .wallet() + .default_signer() + .address(); + let transaction = TransactionRequest::default() + .to(account_address) + .value(U256::from(100_000_000_000_000u128)); + let receipt = provider.send_transaction(transaction).await; + tracing::info!("Sending transaction"); + + let _ = receipt + .expect("Failed to send the transfer transaction") + .get_receipt() + .await + .expect("Failed to get the receipt for the transfer"); + } +}