From 8bf0d69e54376b8503541c99fbe72d982f620770 Mon Sep 17 00:00:00 2001 From: Marios Christou Date: Wed, 8 Oct 2025 16:32:38 +0300 Subject: [PATCH 1/2] Refactor: move chainspec patching and address conversion to common module, remove duplicated logic and tests --- .../node_implementations/common/chainspec.rs | 193 +++++ .../src/node_implementations/common/mod.rs | 3 + .../node_implementations/common/process.rs | 60 ++ .../src/node_implementations/common/revive.rs | 434 +++++++++++ crates/node/src/node_implementations/mod.rs | 2 + .../src/node_implementations/substrate.rs | 704 +----------------- .../src/node_implementations/zombienet.rs | 277 +------ 7 files changed, 756 insertions(+), 917 deletions(-) create mode 100644 crates/node/src/node_implementations/common/chainspec.rs create mode 100644 crates/node/src/node_implementations/common/mod.rs create mode 100644 crates/node/src/node_implementations/common/process.rs create mode 100644 crates/node/src/node_implementations/common/revive.rs diff --git a/crates/node/src/node_implementations/common/chainspec.rs b/crates/node/src/node_implementations/common/chainspec.rs new file mode 100644 index 00000000..3965678b --- /dev/null +++ b/crates/node/src/node_implementations/common/chainspec.rs @@ -0,0 +1,193 @@ +use alloy::{ + genesis::{Genesis, GenesisAccount}, + network::{Ethereum, NetworkWallet}, + primitives::{Address, U256}, +}; +use anyhow::{Context, Result}; +use serde_json::{Value as JsonValue, json}; +use sp_core::crypto::Ss58Codec; +use sp_runtime::AccountId32; +use std::{fs::File, path::Path, process::Command}; + +pub fn export_and_patch_chainspec_json( + binary_path: &Path, + export_command: &str, + chain_arg: &str, + output_path: &Path, + genesis: &mut Genesis, + wallet: &impl NetworkWallet, + initial_balance: u128, +) -> Result<()> { + // Note: we do not pipe the logs of this process to a separate file since this is just a + // once-off export of the default chain spec and not part of the long-running node process. + let output = Command::new(binary_path) + .arg(export_command) + .arg("--chain") + .arg(chain_arg) + .env_remove("RUST_LOG") + .output() + .context("Failed to export the chain-spec")?; + + if !output.status.success() { + anyhow::bail!( + "Export chain-spec failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + let content = + String::from_utf8(output.stdout).context("Failed to decode chain-spec output as UTF-8")?; + let mut chainspec_json: JsonValue = + serde_json::from_str(&content).context("Failed to parse chain spec JSON")?; + + let existing_chainspec_balances = + chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] + .as_array() + .cloned() + .unwrap_or_default(); + + let mut merged_balances: Vec<(String, u128)> = existing_chainspec_balances + .into_iter() + .filter_map(|val| { + if let Some(arr) = val.as_array() { + if arr.len() == 2 { + let account = arr[0].as_str()?.to_string(); + let balance = arr[1].as_f64()? as u128; + return Some((account, balance)); + } + } + None + }) + .collect(); + + for signer_address in wallet.signer_addresses() { + genesis + .alloc + .entry(signer_address) + .or_insert(GenesisAccount::default().with_balance(U256::from(initial_balance))); + } + + let mut eth_balances = + crate::node_implementations::common::chainspec::extract_balance_from_genesis_file(genesis) + .context("Failed to extract balances from EVM genesis JSON")?; + + merged_balances.append(&mut eth_balances); + + chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] = + json!(merged_balances); + + serde_json::to_writer_pretty( + File::create(output_path).context("Failed to create chainspec file")?, + &chainspec_json, + ) + .context("Failed to write chainspec JSON")?; + + Ok(()) +} + +pub fn extract_balance_from_genesis_file(genesis: &Genesis) -> anyhow::Result> { + genesis + .alloc + .iter() + .try_fold(Vec::new(), |mut vec, (address, acc)| { + let substrate_address = eth_to_polkadot_address(address); + let balance = acc.balance.try_into()?; + vec.push((substrate_address, balance)); + Ok(vec) + }) +} + +pub fn eth_to_polkadot_address(address: &Address) -> String { + let eth_bytes = address.0.0; + + let mut padded = [0xEEu8; 32]; + padded[..20].copy_from_slice(ð_bytes); + + let account_id = AccountId32::from(padded); + account_id.to_ss58check() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_genesis_alloc() { + // Create test genesis file + let genesis_json = r#" + { + "alloc": { + "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1": { "balance": "1000000000000000000" }, + "0x0000000000000000000000000000000000000000": { "balance": "0xDE0B6B3A7640000" }, + "0xffffffffffffffffffffffffffffffffffffffff": { "balance": "123456789" } + } + } + "#; + + let result = + extract_balance_from_genesis_file(&serde_json::from_str(genesis_json).unwrap()) + .unwrap(); + + let result_map: std::collections::HashMap<_, _> = result.into_iter().collect(); + + assert_eq!( + result_map.get("5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV"), + Some(&1_000_000_000_000_000_000u128) + ); + + assert_eq!( + result_map.get("5C4hrfjw9DjXZTzV3MwzrrAr9P1MLDHajjSidz9bR544LEq1"), + Some(&1_000_000_000_000_000_000u128) + ); + + assert_eq!( + result_map.get("5HrN7fHLXWcFiXPwwtq2EkSGns9eMmoUQnbVKweNz3VVr6N4"), + Some(&123_456_789u128) + ); + } + + #[test] + fn test_eth_to_polkadot_address() { + let cases = vec![ + ( + "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", + "5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV", + ), + ( + "90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", + "5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV", + ), + ( + "0x0000000000000000000000000000000000000000", + "5C4hrfjw9DjXZTzV3MwzrrAr9P1MLDHajjSidz9bR544LEq1", + ), + ( + "0xffffffffffffffffffffffffffffffffffffffff", + "5HrN7fHLXWcFiXPwwtq2EkSGns9eMmoUQnbVKweNz3VVr6N4", + ), + ]; + + for (eth_addr, expected_ss58) in cases { + let result = eth_to_polkadot_address(ð_addr.parse().unwrap()); + assert_eq!( + result, expected_ss58, + "Mismatch for Ethereum address {eth_addr}" + ); + } + } + + #[test] + fn print_eth_to_polkadot_mappings() { + let eth_addresses = vec![ + "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", + "0xffffffffffffffffffffffffffffffffffffffff", + "90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", + ]; + + for eth_addr in eth_addresses { + let ss58 = eth_to_polkadot_address(ð_addr.parse().unwrap()); + + println!("Ethereum: {eth_addr} -> Polkadot SS58: {ss58}"); + } + } +} diff --git a/crates/node/src/node_implementations/common/mod.rs b/crates/node/src/node_implementations/common/mod.rs new file mode 100644 index 00000000..25a2c940 --- /dev/null +++ b/crates/node/src/node_implementations/common/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod chainspec; +pub(crate) mod process; +pub(crate) mod revive; diff --git a/crates/node/src/node_implementations/common/process.rs b/crates/node/src/node_implementations/common/process.rs new file mode 100644 index 00000000..6cc484c6 --- /dev/null +++ b/crates/node/src/node_implementations/common/process.rs @@ -0,0 +1,60 @@ +use anyhow::Result; +use std::{ + process::{Command, Stdio}, + {path::Path, time::Duration}, +}; + +use crate::helpers::{Process, ProcessReadinessWaitBehavior}; + +const PROXY_LOG_ENV: &str = "info,eth-rpc=debug"; + +pub fn spawn_eth_rpc_process( + logs_directory: &Path, + eth_proxy_binary: &Path, + node_rpc_url: &str, + eth_rpc_port: u16, + extra_args: &[&str], + ready_marker: &str, +) -> anyhow::Result { + let ready_marker = ready_marker.to_owned(); + Process::new( + "proxy", + logs_directory, + eth_proxy_binary, + |command, stdout_file, stderr_file| { + command + .arg("--node-rpc-url") + .arg(node_rpc_url) + .arg("--rpc-cors") + .arg("all") + .arg("--rpc-max-connections") + .arg(u32::MAX.to_string()) + .arg("--rpc-port") + .arg(eth_rpc_port.to_string()) + .env("RUST_LOG", PROXY_LOG_ENV); + for arg in extra_args { + command.arg(arg); + } + command.stdout(stdout_file).stderr(stderr_file); + }, + ProcessReadinessWaitBehavior::TimeBoundedWaitFunction { + max_wait_duration: Duration::from_secs(30), + check_function: Box::new(move |_, stderr_line| match stderr_line { + Some(line) => Ok(line.contains(&ready_marker)), + None => Ok(false), + }), + }, + ) +} + +pub fn command_version(command_binary: &Path) -> Result { + let output = Command::new(command_binary) + .arg("--version") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn()? + .wait_with_output()? + .stdout; + Ok(String::from_utf8_lossy(&output).trim().to_string()) +} diff --git a/crates/node/src/node_implementations/common/revive.rs b/crates/node/src/node_implementations/common/revive.rs new file mode 100644 index 00000000..24e213f0 --- /dev/null +++ b/crates/node/src/node_implementations/common/revive.rs @@ -0,0 +1,434 @@ +use alloy::{ + consensus::{BlockHeader, TxEnvelope}, + network::{ + Ethereum, Network, TransactionBuilder, TransactionBuilderError, UnbuiltTransactionError, + }, + primitives::{Address, B64, B256, BlockNumber, Bloom, Bytes, U256}, + rpc::types::eth::{Block, Header, Transaction}, +}; + +use serde::{Deserialize, Serialize}; + +#[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 + } +} diff --git a/crates/node/src/node_implementations/mod.rs b/crates/node/src/node_implementations/mod.rs index f44813a1..29276d20 100644 --- a/crates/node/src/node_implementations/mod.rs +++ b/crates/node/src/node_implementations/mod.rs @@ -1,3 +1,5 @@ +mod common; + pub mod geth; pub mod lighthouse_geth; pub mod substrate; diff --git a/crates/node/src/node_implementations/substrate.rs b/crates/node/src/node_implementations/substrate.rs index a92b3da9..c1176e1d 100644 --- a/crates/node/src/node_implementations/substrate.rs +++ b/crates/node/src/node_implementations/substrate.rs @@ -2,7 +2,6 @@ use std::{ fs::{create_dir_all, remove_dir_all}, path::PathBuf, pin::Pin, - process::{Command, Stdio}, sync::{ Arc, atomic::{AtomicU32, Ordering}, @@ -11,17 +10,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, - }, + genesis::Genesis, + network::EthereumWallet, + primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256}, providers::{ Provider, ext::DebugApi, @@ -29,7 +21,6 @@ use alloy::{ }, rpc::types::{ EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest, - eth::{Block, Header, Transaction}, trace::geth::{ DiffMode, GethDebugTracingOptions, GethTrace, PreStateConfig, PreStateFrame, }, @@ -39,13 +30,8 @@ use anyhow::Context as _; use futures::{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_format::traits::ResolverApi; use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation}; use tokio::sync::OnceCell; use tracing::instrument; @@ -54,6 +40,11 @@ use crate::{ Node, constants::{CHAIN_ID, INITIAL_BALANCE}, helpers::{Process, ProcessReadinessWaitBehavior}, + node_implementations::common::{ + chainspec::export_and_patch_chainspec_json, + process::{command_version, spawn_eth_rpc_process}, + revive::ReviveNetwork, + }, provider_utils::{ ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider, execute_transaction, @@ -94,7 +85,6 @@ impl SubstrateNode { const BASE_PROXY_RPC_PORT: u16 = 8545; const SUBSTRATE_LOG_ENV: &str = "error,evm=debug,sc_rpc_server=info,runtime::revive=debug"; - const PROXY_LOG_ENV: &str = "info,eth-rpc=debug"; pub const KITCHENSINK_EXPORT_CHAINSPEC_COMMAND: &str = "export-chain-spec"; pub const REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND: &str = "build-spec"; @@ -146,72 +136,16 @@ impl SubstrateNode { let template_chainspec_path = self.base_directory.join(Self::CHAIN_SPEC_JSON_FILE); - // Note: we do not pipe the logs of this process to a separate file since this is just a - // once-off export of the default chain spec and not part of the long-running node process. - let output = Command::new(&self.node_binary) - .arg(self.export_chainspec_command.as_str()) - .arg("--chain") - .arg("dev") - .env_remove("RUST_LOG") - .output() - .context("Failed to export the chain-spec")?; - - if !output.status.success() { - anyhow::bail!( - "Substrate-node export-chain-spec failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } + export_and_patch_chainspec_json( + &self.node_binary, + self.export_chainspec_command.as_str(), + "dev", + &template_chainspec_path, + &mut genesis, + &self.wallet, + INITIAL_BALANCE, + )?; - let content = String::from_utf8(output.stdout) - .context("Failed to decode Substrate export-chain-spec output as UTF-8")?; - let mut chainspec_json: JsonValue = - serde_json::from_str(&content).context("Failed to parse Substrate chain spec JSON")?; - - let existing_chainspec_balances = - chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] - .as_array() - .cloned() - .unwrap_or_default(); - - let mut merged_balances: Vec<(String, u128)> = existing_chainspec_balances - .into_iter() - .filter_map(|val| { - if let Some(arr) = val.as_array() { - if arr.len() == 2 { - let account = arr[0].as_str()?.to_string(); - let balance = arr[1].as_f64()? as u128; - return Some((account, balance)); - } - } - None - }) - .collect(); - let mut eth_balances = { - for signer_address in - >::signer_addresses(&self.wallet) - { - // Note, the use of the entry API here means that we only modify the entries for any - // account that is not in the `alloc` field of the genesis state. - genesis - .alloc - .entry(signer_address) - .or_insert(GenesisAccount::default().with_balance(U256::from(INITIAL_BALANCE))); - } - self.extract_balance_from_genesis_file(&genesis) - .context("Failed to extract balances from EVM genesis JSON")? - }; - merged_balances.append(&mut eth_balances); - - chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] = - json!(merged_balances); - - serde_json::to_writer_pretty( - std::fs::File::create(&template_chainspec_path) - .context("Failed to create substrate template chainspec file")?, - &chainspec_json, - ) - .context("Failed to write substrate template chainspec JSON")?; Ok(self) } @@ -266,32 +200,15 @@ impl SubstrateNode { return Err(err); } } - - let eth_proxy_process = Process::new( - "proxy", + let eth_proxy_process = spawn_eth_rpc_process( self.logs_directory.as_path(), self.eth_proxy_binary.as_path(), - |command, stdout_file, stderr_file| { - command - .arg("--dev") - .arg("--rpc-port") - .arg(proxy_rpc_port.to_string()) - .arg("--node-rpc-url") - .arg(format!("ws://127.0.0.1:{substrate_rpc_port}")) - .arg("--rpc-max-connections") - .arg(u32::MAX.to_string()) - .env("RUST_LOG", Self::PROXY_LOG_ENV) - .stdout(stdout_file) - .stderr(stderr_file); - }, - ProcessReadinessWaitBehavior::TimeBoundedWaitFunction { - max_wait_duration: Duration::from_secs(30), - check_function: Box::new(|_, stderr_line| match stderr_line { - Some(line) => Ok(line.contains(Self::ETH_PROXY_READY_MARKER)), - None => Ok(false), - }), - }, + &format!("ws://127.0.0.1:{substrate_rpc_port}"), + proxy_rpc_port, + &["--dev"], // extra args + Self::ETH_PROXY_READY_MARKER, ); + match eth_proxy_process { Ok(process) => self.eth_proxy_process = Some(process), Err(err) => { @@ -305,41 +222,8 @@ impl SubstrateNode { Ok(()) } - fn extract_balance_from_genesis_file( - &self, - genesis: &Genesis, - ) -> anyhow::Result> { - genesis - .alloc - .iter() - .try_fold(Vec::new(), |mut vec, (address, acc)| { - let substrate_address = Self::eth_to_substrate_address(address); - let balance = acc.balance.try_into()?; - vec.push((substrate_address, balance)); - Ok(vec) - }) - } - - fn eth_to_substrate_address(address: &Address) -> String { - let eth_bytes = address.0.0; - - let mut padded = [0xEEu8; 32]; - padded[..20].copy_from_slice(ð_bytes); - - let account_id = AccountId32::from(padded); - account_id.to_ss58check() - } - pub fn eth_rpc_version(&self) -> anyhow::Result { - let output = Command::new(&self.eth_proxy_binary) - .arg("--version") - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn()? - .wait_with_output()? - .stdout; - Ok(String::from_utf8_lossy(&output).trim().to_string()) + command_version(&self.eth_proxy_binary) } async fn provider( @@ -689,17 +573,7 @@ impl Node for SubstrateNode { } fn version(&self) -> anyhow::Result { - let output = Command::new(&self.node_binary) - .arg("--version") - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn() - .context("Failed to spawn substrate --version")? - .wait_with_output() - .context("Failed to wait for substrate --version")? - .stdout; - Ok(String::from_utf8_lossy(&output).into()) + command_version(&self.node_binary) } } @@ -709,430 +583,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; @@ -1141,7 +591,7 @@ mod tests { use std::fs; use super::*; - use crate::Node; + use crate::node_implementations::common::chainspec::eth_to_polkadot_address; fn test_config() -> TestExecutionContext { TestExecutionContext::default() @@ -1252,12 +702,10 @@ mod tests { let contents = fs::read_to_string(&final_chainspec_path).expect("Failed to read chainspec"); // Validate that the Substrate addresses derived from the Ethereum addresses are in the file - let first_eth_addr = SubstrateNode::eth_to_substrate_address( - &"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap(), - ); - let second_eth_addr = SubstrateNode::eth_to_substrate_address( - &"Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2".parse().unwrap(), - ); + let first_eth_addr = + eth_to_polkadot_address(&"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap()); + let second_eth_addr = + eth_to_polkadot_address(&"Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2".parse().unwrap()); assert!( contents.contains(&first_eth_addr), @@ -1269,96 +717,6 @@ mod tests { ); } - #[test] - #[ignore = "Ignored since they take a long time to run"] - fn test_parse_genesis_alloc() { - // Create test genesis file - let genesis_json = r#" - { - "alloc": { - "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1": { "balance": "1000000000000000000" }, - "0x0000000000000000000000000000000000000000": { "balance": "0xDE0B6B3A7640000" }, - "0xffffffffffffffffffffffffffffffffffffffff": { "balance": "123456789" } - } - } - "#; - - let context = test_config(); - let node = SubstrateNode::new( - context.kitchensink_configuration.path.clone(), - SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND, - &context, - ); - - let result = node - .extract_balance_from_genesis_file(&serde_json::from_str(genesis_json).unwrap()) - .unwrap(); - - let result_map: std::collections::HashMap<_, _> = result.into_iter().collect(); - - assert_eq!( - result_map.get("5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV"), - Some(&1_000_000_000_000_000_000u128) - ); - - assert_eq!( - result_map.get("5C4hrfjw9DjXZTzV3MwzrrAr9P1MLDHajjSidz9bR544LEq1"), - Some(&1_000_000_000_000_000_000u128) - ); - - assert_eq!( - result_map.get("5HrN7fHLXWcFiXPwwtq2EkSGns9eMmoUQnbVKweNz3VVr6N4"), - Some(&123_456_789u128) - ); - } - - #[test] - #[ignore = "Ignored since they take a long time to run"] - fn print_eth_to_substrate_mappings() { - let eth_addresses = vec![ - "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", - "0xffffffffffffffffffffffffffffffffffffffff", - "90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", - ]; - - for eth_addr in eth_addresses { - let ss58 = SubstrateNode::eth_to_substrate_address(ð_addr.parse().unwrap()); - - println!("Ethereum: {eth_addr} -> Substrate SS58: {ss58}"); - } - } - - #[test] - #[ignore = "Ignored since they take a long time to run"] - fn test_eth_to_substrate_address() { - let cases = vec![ - ( - "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", - "5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV", - ), - ( - "90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", - "5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV", - ), - ( - "0x0000000000000000000000000000000000000000", - "5C4hrfjw9DjXZTzV3MwzrrAr9P1MLDHajjSidz9bR544LEq1", - ), - ( - "0xffffffffffffffffffffffffffffffffffffffff", - "5HrN7fHLXWcFiXPwwtq2EkSGns9eMmoUQnbVKweNz3VVr6N4", - ), - ]; - - for (eth_addr, expected_ss58) in cases { - let result = SubstrateNode::eth_to_substrate_address(ð_addr.parse().unwrap()); - assert_eq!( - result, expected_ss58, - "Mismatch for Ethereum address {eth_addr}" - ); - } - } - #[test] #[ignore = "Ignored since they take a long time to run"] fn version_works() { diff --git a/crates/node/src/node_implementations/zombienet.rs b/crates/node/src/node_implementations/zombienet.rs index ca7e3a15..3e7911dc 100644 --- a/crates/node/src/node_implementations/zombienet.rs +++ b/crates/node/src/node_implementations/zombienet.rs @@ -30,7 +30,6 @@ use std::{ fs::{create_dir_all, remove_dir_all}, path::PathBuf, pin::Pin, - process::{Command, Stdio}, sync::{ Arc, atomic::{AtomicU32, Ordering}, @@ -40,8 +39,8 @@ use std::{ use alloy::{ eips::BlockNumberOrTag, - genesis::{Genesis, GenesisAccount}, - network::{Ethereum, EthereumWallet, NetworkWallet}, + genesis::Genesis, + network::EthereumWallet, primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256}, providers::{ Provider, @@ -61,9 +60,6 @@ use revive_dt_common::fs::clear_directory; use revive_dt_config::*; use revive_dt_format::traits::ResolverApi; use revive_dt_node_interaction::{EthereumNode, MinedBlockInformation}; -use serde_json::{Value as JsonValue, json}; -use sp_core::crypto::Ss58Codec; -use sp_runtime::AccountId32; use tokio::sync::OnceCell; use tracing::instrument; use zombienet_sdk::{LocalFileSystem, NetworkConfigBuilder, NetworkConfigExt}; @@ -71,8 +67,12 @@ use zombienet_sdk::{LocalFileSystem, NetworkConfigBuilder, NetworkConfigExt}; use crate::{ Node, constants::INITIAL_BALANCE, - helpers::{Process, ProcessReadinessWaitBehavior}, - node_implementations::substrate::ReviveNetwork, + helpers::Process, + node_implementations::common::{ + chainspec::export_and_patch_chainspec_json, + process::{command_version, spawn_eth_rpc_process}, + revive::ReviveNetwork, + }, provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider}, }; @@ -159,7 +159,7 @@ impl ZombieNode { } } - fn init(&mut self, genesis: Genesis) -> anyhow::Result<&mut Self> { + fn init(&mut self, mut genesis: Genesis) -> anyhow::Result<&mut Self> { let _ = clear_directory(&self.base_directory); let _ = clear_directory(&self.logs_directory); @@ -169,7 +169,16 @@ impl ZombieNode { .context("Failed to create logs directory for zombie node")?; let template_chainspec_path = self.base_directory.join(Self::CHAIN_SPEC_JSON_FILE); - self.prepare_chainspec(template_chainspec_path.clone(), genesis)?; + export_and_patch_chainspec_json( + &self.polkadot_parachain_path, + Self::EXPORT_CHAINSPEC_COMMAND, + "asset-hub-westend-local", + &template_chainspec_path, + &mut genesis, + &self.wallet, + INITIAL_BALANCE, + )?; + let polkadot_parachain_path = self .polkadot_parachain_path .to_str() @@ -223,30 +232,13 @@ impl ZombieNode { let node_url = format!("ws://localhost:{}", self.node_rpc_port.unwrap()); let eth_rpc_port = Self::ETH_RPC_BASE_PORT + self.id as u16; - let eth_rpc_process = Process::new( - "proxy", + let eth_rpc_process = spawn_eth_rpc_process( self.logs_directory.as_path(), self.eth_proxy_binary.as_path(), - |command, stdout_file, stderr_file| { - command - .arg("--node-rpc-url") - .arg(node_url) - .arg("--rpc-cors") - .arg("all") - .arg("--rpc-max-connections") - .arg(u32::MAX.to_string()) - .arg("--rpc-port") - .arg(eth_rpc_port.to_string()) - .stdout(stdout_file) - .stderr(stderr_file); - }, - ProcessReadinessWaitBehavior::TimeBoundedWaitFunction { - max_wait_duration: Duration::from_secs(30), - check_function: Box::new(|_, stderr_line| match stderr_line { - Some(line) => Ok(line.contains(Self::ETH_RPC_READY_MARKER)), - None => Ok(false), - }), - }, + &node_url, + eth_rpc_port, + &[], // extra args + Self::ETH_RPC_READY_MARKER, ); match eth_rpc_process { @@ -267,115 +259,8 @@ impl ZombieNode { Ok(()) } - fn prepare_chainspec( - &mut self, - template_chainspec_path: PathBuf, - mut genesis: Genesis, - ) -> anyhow::Result<()> { - let mut cmd: Command = std::process::Command::new(&self.polkadot_parachain_path); - cmd.arg(Self::EXPORT_CHAINSPEC_COMMAND) - .arg("--chain") - .arg("asset-hub-westend-local"); - - let output = cmd.output().context("Failed to export the chain-spec")?; - - if !output.status.success() { - anyhow::bail!( - "Build chain-spec failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - let content = String::from_utf8(output.stdout) - .context("Failed to decode collators chain-spec output as UTF-8")?; - let mut chainspec_json: JsonValue = - serde_json::from_str(&content).context("Failed to parse collators chain spec JSON")?; - - let existing_chainspec_balances = - chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] - .as_array() - .cloned() - .unwrap_or_default(); - - let mut merged_balances: Vec<(String, u128)> = existing_chainspec_balances - .into_iter() - .filter_map(|val| { - if let Some(arr) = val.as_array() { - if arr.len() == 2 { - let account = arr[0].as_str()?.to_string(); - let balance = arr[1].as_f64()? as u128; - return Some((account, balance)); - } - } - None - }) - .collect(); - - let mut eth_balances = { - for signer_address in - >::signer_addresses(&self.wallet) - { - // Note, the use of the entry API here means that we only modify the entries for any - // account that is not in the `alloc` field of the genesis state. - genesis - .alloc - .entry(signer_address) - .or_insert(GenesisAccount::default().with_balance(U256::from(INITIAL_BALANCE))); - } - self.extract_balance_from_genesis_file(&genesis) - .context("Failed to extract balances from EVM genesis JSON")? - }; - - merged_balances.append(&mut eth_balances); - - chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] = - json!(merged_balances); - - let writer = std::fs::File::create(&template_chainspec_path) - .context("Failed to create template chainspec file")?; - - serde_json::to_writer_pretty(writer, &chainspec_json) - .context("Failed to write template chainspec JSON")?; - - Ok(()) - } - - fn extract_balance_from_genesis_file( - &self, - genesis: &Genesis, - ) -> anyhow::Result> { - genesis - .alloc - .iter() - .try_fold(Vec::new(), |mut vec, (address, acc)| { - let polkadot_address = Self::eth_to_polkadot_address(address); - let balance = acc.balance.try_into()?; - vec.push((polkadot_address, balance)); - Ok(vec) - }) - } - - fn eth_to_polkadot_address(address: &Address) -> String { - let eth_bytes = address.0.0; - - let mut padded = [0xEEu8; 32]; - padded[..20].copy_from_slice(ð_bytes); - - let account_id = AccountId32::from(padded); - account_id.to_ss58check() - } - pub fn eth_rpc_version(&self) -> anyhow::Result { - let output = Command::new(&self.eth_proxy_binary) - .arg("--version") - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn()? - .wait_with_output()? - .stdout; - - Ok(String::from_utf8_lossy(&output).trim().to_string()) + command_version(&self.eth_proxy_binary) } async fn provider( @@ -748,17 +633,7 @@ impl Node for ZombieNode { } fn version(&self) -> anyhow::Result { - let output = Command::new(&self.polkadot_parachain_path) - .arg("--version") - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn() - .context("Failed execute --version")? - .wait_with_output() - .context("Failed to wait --version")? - .stdout; - Ok(String::from_utf8_lossy(&output).into()) + command_version(&self.polkadot_parachain_path) } } @@ -772,7 +647,9 @@ impl Drop for ZombieNode { mod tests { use alloy::rpc::types::TransactionRequest; - use crate::node_implementations::zombienet::tests::utils::shared_node; + use crate::node_implementations::{ + common::chainspec::eth_to_polkadot_address, zombienet::tests::utils::shared_node, + }; use super::*; @@ -873,12 +750,10 @@ mod tests { std::fs::read_to_string(&final_chainspec_path).expect("Failed to read chainspec"); // Validate that the Polkadot addresses derived from the Ethereum addresses are in the file - let first_eth_addr = ZombieNode::eth_to_polkadot_address( - &"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap(), - ); - let second_eth_addr = ZombieNode::eth_to_polkadot_address( - &"Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2".parse().unwrap(), - ); + let first_eth_addr = + eth_to_polkadot_address(&"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap()); + let second_eth_addr = + eth_to_polkadot_address(&"Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2".parse().unwrap()); assert!( contents.contains(&first_eth_addr), @@ -890,92 +765,6 @@ mod tests { ); } - #[tokio::test] - async fn test_parse_genesis_alloc() { - // Create test genesis file - let genesis_json = r#" - { - "alloc": { - "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1": { "balance": "1000000000000000000" }, - "0x0000000000000000000000000000000000000000": { "balance": "0xDE0B6B3A7640000" }, - "0xffffffffffffffffffffffffffffffffffffffff": { "balance": "123456789" } - } - } - "#; - - let context = test_config(); - let node = ZombieNode::new( - context.polkadot_parachain_configuration.path.clone(), - &context, - ); - - let result = node - .extract_balance_from_genesis_file(&serde_json::from_str(genesis_json).unwrap()) - .unwrap(); - - let result_map: std::collections::HashMap<_, _> = result.into_iter().collect(); - - assert_eq!( - result_map.get("5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV"), - Some(&1_000_000_000_000_000_000u128) - ); - - assert_eq!( - result_map.get("5C4hrfjw9DjXZTzV3MwzrrAr9P1MLDHajjSidz9bR544LEq1"), - Some(&1_000_000_000_000_000_000u128) - ); - - assert_eq!( - result_map.get("5HrN7fHLXWcFiXPwwtq2EkSGns9eMmoUQnbVKweNz3VVr6N4"), - Some(&123_456_789u128) - ); - } - - #[test] - fn print_eth_to_polkadot_mappings() { - let eth_addresses = vec![ - "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", - "0xffffffffffffffffffffffffffffffffffffffff", - "90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", - ]; - - for eth_addr in eth_addresses { - let ss58 = ZombieNode::eth_to_polkadot_address(ð_addr.parse().unwrap()); - - println!("Ethereum: {eth_addr} -> Polkadot SS58: {ss58}"); - } - } - - #[test] - fn test_eth_to_polkadot_address() { - let cases = vec![ - ( - "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", - "5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV", - ), - ( - "90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", - "5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV", - ), - ( - "0x0000000000000000000000000000000000000000", - "5C4hrfjw9DjXZTzV3MwzrrAr9P1MLDHajjSidz9bR544LEq1", - ), - ( - "0xffffffffffffffffffffffffffffffffffffffff", - "5HrN7fHLXWcFiXPwwtq2EkSGns9eMmoUQnbVKweNz3VVr6N4", - ), - ]; - - for (eth_addr, expected_ss58) in cases { - let result = ZombieNode::eth_to_polkadot_address(ð_addr.parse().unwrap()); - assert_eq!( - result, expected_ss58, - "Mismatch for Ethereum address {eth_addr}" - ); - } - } - #[test] fn eth_rpc_version_works() { // Arrange From c3c128c45aed013c2f1ae24acc4f1cb26099e6ac Mon Sep 17 00:00:00 2001 From: Marios Christou Date: Thu, 9 Oct 2025 17:39:41 +0300 Subject: [PATCH 2/2] fmt --- crates/node/src/node_implementations/zombienet.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/node/src/node_implementations/zombienet.rs b/crates/node/src/node_implementations/zombienet.rs index 522e0090..05310186 100644 --- a/crates/node/src/node_implementations/zombienet.rs +++ b/crates/node/src/node_implementations/zombienet.rs @@ -74,7 +74,10 @@ use crate::{ process::{command_version, spawn_eth_rpc_process}, revive::ReviveNetwork, }, - provider_utils::{ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider}, + provider_utils::{ + ConcreteProvider, FallbackGasFiller, construct_concurrency_limited_provider, + execute_transaction, + }, }; static NODE_COUNT: AtomicU32 = AtomicU32::new(0); @@ -120,8 +123,6 @@ impl ZombienetNode { const PARACHAIN_ID: u32 = 100; const ETH_RPC_BASE_PORT: u16 = 8545; - const PROXY_LOG_ENV: &str = "info,eth-rpc=debug"; - const ETH_RPC_READY_MARKER: &str = "Running JSON-RPC server"; const EXPORT_CHAINSPEC_COMMAND: &str = "build-spec";