diff --git a/Cargo.lock b/Cargo.lock index 425d9d73de310..a383c7093d753 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1211,6 +1211,7 @@ dependencies = [ "futures-timer", "hyper 1.6.0", "itertools 0.14.0", + "jsonrpsee", "libsecp256k1", "op-alloy-consensus 0.17.2", "op-alloy-rpc-types", @@ -14345,13 +14346,16 @@ dependencies = [ "sc-executor-common", "sc-executor-wasmtime", "sc-keystore", + "sc-network", "sc-network-types", "sc-rpc", "sc-rpc-api", "sc-rpc-server", "sc-rpc-spec-v2", + "sc-runtime-utilities", "sc-service", "sc-state-db", + "sc-telemetry", "sc-tracing", "sc-transaction-pool", "sc-transaction-pool-api", @@ -17196,6 +17200,21 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "sc-runtime-utilities" +version = "0.1.0" +source = "git+https://github.com/paritytech/polkadot-sdk.git?branch=master#e8f1aff5a174f420cdd77f5d5c854dd6dc8a3273" +dependencies = [ + "parity-scale-codec", + "sc-executor", + "sc-executor-common", + "sp-core", + "sp-crypto-hashing 0.1.0 (git+https://github.com/paritytech/polkadot-sdk.git?branch=master)", + "sp-state-machine", + "sp-wasm-interface", + "thiserror 1.0.69", +] + [[package]] name = "sc-service" version = "0.35.0" diff --git a/crates/anvil-polkadot/Cargo.toml b/crates/anvil-polkadot/Cargo.toml index 5dbd70af0fa49..55f3bfe3202cc 100644 --- a/crates/anvil-polkadot/Cargo.toml +++ b/crates/anvil-polkadot/Cargo.toml @@ -19,6 +19,7 @@ path = "bin/main.rs" [dependencies] # foundry internal +codec = { version = "3.7.5", default-features = true, package = "parity-scale-codec" } substrate-runtime = { path = "substrate-runtime" } secp256k1 = { version = "0.28.0", default-features = false } libsecp256k1 = { version = "0.7.0", default-features = false } @@ -37,13 +38,16 @@ polkadot-sdk = { git = "https://github.com/paritytech/polkadot-sdk.git", branch "sc-executor-common", "sc-executor-wasmtime", "sc-keystore", + "sc-network", "sc-network-types", "sc-rpc", "sc-rpc-api", "sc-rpc-server", "sc-rpc-spec-v2", + "sc-runtime-utilities", "sc-service", "sc-state-db", + "sc-telemetry", "sc-tracing", "sc-transaction-pool", "sc-transaction-pool-api", @@ -131,10 +135,10 @@ clap = { version = "4", features = [ clap_complete = { version = "4" } chrono.workspace = true clap_complete_fig = "4" -parity-scale-codec = "3.7.5" subxt = "0.43.0" subxt-signer = "0.43.0" tokio-stream = "0.1.17" +jsonrpsee = "0.24.9" sqlx = "0.8.6" [dev-dependencies] diff --git a/crates/anvil-polkadot/src/api_server/error.rs b/crates/anvil-polkadot/src/api_server/error.rs index 3de255d6015f8..93366335e7044 100644 --- a/crates/anvil-polkadot/src/api_server/error.rs +++ b/crates/anvil-polkadot/src/api_server/error.rs @@ -13,6 +13,8 @@ pub enum Error { InvalidParams(String), #[error("Revive call failed: {0}")] ReviveRpc(#[from] EthRpcError), + #[error("Internal error: {0}")] + InternalError(String), } impl From for Error { fn from(err: subxt::Error) -> Self { @@ -72,6 +74,9 @@ impl ToRpcResponseResult for Result { Error::ReviveRpc(client_error) => { RpcError::internal_error_with(format!("{client_error}")).into() } + Error::InternalError(error_message) => { + RpcError::internal_error_with(error_message).into() + } }, } } diff --git a/crates/anvil-polkadot/src/api_server/server.rs b/crates/anvil-polkadot/src/api_server/server.rs index 390723990f662..e8c1f811bf81f 100644 --- a/crates/anvil-polkadot/src/api_server/server.rs +++ b/crates/anvil-polkadot/src/api_server/server.rs @@ -9,8 +9,10 @@ use crate::{ logging::LoggingManager, macros::node_info, substrate_node::{ - impersonation::ImpersonationManager, in_mem_rpc::InMemoryRpcClient, - mining_engine::MiningEngine, service::Service, + impersonation::ImpersonationManager, + in_mem_rpc::InMemoryRpcClient, + mining_engine::MiningEngine, + service::{Backend, Service}, }, }; use alloy_eips::{BlockId, BlockNumberOrTag}; @@ -19,6 +21,7 @@ use alloy_rpc_types::{TransactionRequest, anvil::MineOptions}; use alloy_serde::WithOtherFields; use anvil_core::eth::{EthRequest, Params as MineParams}; use anvil_rpc::response::ResponseResult; +use codec::Decode; use futures::{StreamExt, channel::mpsc}; use polkadot_sdk::{ pallet_revive::evm::{Account, Block, Bytes, ReceiptInfo, TransactionSigned}, @@ -27,12 +30,16 @@ use polkadot_sdk::{ client::{Client as EthRpcClient, ClientError, SubscriptionType}, subxt_client::{self, SrcChainConfig}, }, + parachains_common::Hash, + sc_client_api::{Backend as _, HeaderBackend, StateBackend, TrieCacheContext}, + sp_api::{Metadata, ProvideRuntimeApi}, sp_core::{self, keccak_256}, }; use sqlx::sqlite::SqlitePoolOptions; use std::{sync::Arc, time::Duration}; use subxt::{ - OnlineClient, backend::rpc::RpcClient, config::substrate::H256, + Metadata as SubxtMetadata, OnlineClient, backend::rpc::RpcClient, + client::RuntimeVersion as SubxtRuntimeVersion, config::substrate::H256, ext::subxt_rpcs::LegacyRpcMethods, utils::H160, }; @@ -43,6 +50,7 @@ pub struct Wallet { pub struct ApiServer { req_receiver: mpsc::Receiver, logging_manager: LoggingManager, + backend: Arc, mining_engine: Arc, eth_rpc_client: EthRpcClient, wallet: Wallet, @@ -61,6 +69,7 @@ impl ApiServer { Ok(Self { req_receiver, logging_manager, + backend: substrate_service.backend.clone(), mining_engine: substrate_service.mining_engine.clone(), eth_rpc_client, impersonation_manager, @@ -257,12 +266,14 @@ impl ApiServer { // Eth RPCs fn eth_chain_id(&self) -> Result { node_info!("eth_chainId"); - Ok(U256::from(self.eth_rpc_client.chain_id()).to::()) + let latest_block_hash = self.backend.blockchain().info().best_hash; + Ok(U256::from(self.chain_id(latest_block_hash)).to::()) } fn network_id(&self) -> Result { node_info!("eth_networkId"); - Ok(self.eth_rpc_client.chain_id()) + let latest_block_hash = self.backend.blockchain().info().best_hash; + Ok(self.chain_id(latest_block_hash)) } fn net_listening(&self) -> Result { @@ -446,11 +457,83 @@ impl ApiServer { self.impersonation_manager.stop_impersonating(addr); Ok(()) } + + fn chain_id(&self, at: Hash) -> u64 { + let chain_id_key: [u8; 16] = [ + 149u8, 39u8, 54u8, 105u8, 39u8, 71u8, 142u8, 113u8, 13u8, 63u8, 127u8, 183u8, 124u8, + 109u8, 31u8, 137u8, + ]; + if let Ok(state_at) = self.backend.state_at(at, TrieCacheContext::Trusted) + && let Ok(Some(encoded_chain_id)) = state_at.storage(chain_id_key.as_slice()) + && let Ok(chain_id) = u64::decode(&mut &encoded_chain_id[..]) + { + return chain_id; + } + + // if the chain id is not found, use the default chain id + self.eth_rpc_client.chain_id() + } } async fn create_revive_rpc_client(substrate_service: &Service) -> Result { let rpc_client = RpcClient::new(InMemoryRpcClient(substrate_service.rpc_handlers.clone())); - let api = OnlineClient::::from_rpc_client(rpc_client.clone()).await?; + + let genesis_block_number = substrate_service.genesis_block_number.try_into().map_err(|_| { + Error::InternalError(format!( + "Genesis block number {} is too large for u32 (max: {})", + substrate_service.genesis_block_number, + u32::MAX + )) + })?; + + let Some(genesis_hash) = substrate_service.client.hash(genesis_block_number).ok().flatten() + else { + return Err(Error::InternalError(format!( + "Genesis hash not found for genesis block number {}", + substrate_service.genesis_block_number + ))); + }; + + let Ok(runtime_version) = substrate_service.client.runtime_version_at(genesis_hash) else { + return Err(Error::InternalError( + "Runtime version not found for given genesis hash".to_string(), + )); + }; + + let subxt_runtime_version = SubxtRuntimeVersion { + spec_version: runtime_version.spec_version, + transaction_version: runtime_version.transaction_version, + }; + + let Ok(supported_metadata_versions) = + substrate_service.client.runtime_api().metadata_versions(genesis_hash) + else { + return Err(Error::InternalError("Unable to fetch metadata versions".to_string())); + }; + let Some(latest_metadata_version) = supported_metadata_versions.into_iter().max() else { + return Err(Error::InternalError("No stable metadata versions supported".to_string())); + }; + let opaque_metadata = substrate_service + .client + .runtime_api() + .metadata_at_version(genesis_hash, latest_metadata_version) + .map_err(|_| { + Error::InternalError("Failed to get runtime API for genesis hash".to_string()) + })? + .ok_or_else(|| { + Error::InternalError(format!( + "Metadata not found for version {latest_metadata_version} at genesis hash" + )) + })?; + let subxt_metadata = SubxtMetadata::decode(&mut (*opaque_metadata).as_slice()) + .map_err(|_| Error::InternalError("Unable to decode metadata".to_string()))?; + + let api = OnlineClient::::from_rpc_client_with( + genesis_hash, + subxt_runtime_version, + subxt_metadata, + rpc_client.clone(), + )?; let rpc = LegacyRpcMethods::::new(rpc_client.clone()); let block_provider = SubxtBlockInfoProvider::new(api.clone(), rpc.clone()).await?; diff --git a/crates/anvil-polkadot/src/lib.rs b/crates/anvil-polkadot/src/lib.rs index 2fdd3bdba325c..0a7d314fadd6a 100644 --- a/crates/anvil-polkadot/src/lib.rs +++ b/crates/anvil-polkadot/src/lib.rs @@ -6,7 +6,7 @@ use crate::{ api_server::ApiHandle, config::AnvilNodeConfig, logging::{LoggingManager, NodeLogLayer}, - substrate_node::service::Service, + substrate_node::{genesis::GenesisConfig, service::Service}, }; use clap::{CommandFactory, Parser}; use eyre::Result; @@ -83,10 +83,12 @@ pub fn run_command(args: Anvil) -> Result<()> { } return Ok(()); } - let substrate_client = opts::SubstrateCli {}; let (anvil_config, substrate_config) = args.node.into_node_config()?; + let substrate_client = + opts::SubstrateCli { genesis_config: GenesisConfig::from(&anvil_config) }; + let tokio_runtime = build_runtime()?; let signals = tokio_runtime.block_on(async { sc_cli::Signals::capture() })?; diff --git a/crates/anvil-polkadot/src/opts.rs b/crates/anvil-polkadot/src/opts.rs index 2bc0d40b5be37..e5af691e2f786 100644 --- a/crates/anvil-polkadot/src/opts.rs +++ b/crates/anvil-polkadot/src/opts.rs @@ -1,4 +1,7 @@ -use crate::{cmd::NodeArgs, substrate_node::chain_spec}; +use crate::{ + cmd::NodeArgs, + substrate_node::{chain_spec, genesis::GenesisConfig}, +}; use clap::{Parser, Subcommand}; use foundry_cli::opts::GlobalArgs; use foundry_common::version::{LONG_VERSION, SHORT_VERSION}; @@ -31,7 +34,10 @@ pub enum AnvilSubcommand { GenerateFigSpec, } -pub struct SubstrateCli; +pub struct SubstrateCli { + // Used to inject the anvil config into the chain spec + pub genesis_config: GenesisConfig, +} // Implementation of the SubstrateCli, which enables us to launch an in-process substrate node. impl sc_cli::SubstrateCli for SubstrateCli { @@ -63,12 +69,7 @@ impl sc_cli::SubstrateCli for SubstrateCli { "anvil-polkadot".into() } - fn load_spec(&self, id: &str) -> std::result::Result, String> { - Ok(match id { - "dev" | "" => Box::new(chain_spec::development_chain_spec()?), - path => { - Box::new(chain_spec::ChainSpec::from_json_file(std::path::PathBuf::from(path))?) - } - }) + fn load_spec(&self, _: &str) -> std::result::Result, String> { + Ok(Box::new(chain_spec::development_chain_spec(self.genesis_config.clone())?)) } } diff --git a/crates/anvil-polkadot/src/substrate_node/chain_spec.rs b/crates/anvil-polkadot/src/substrate_node/chain_spec.rs index 0acb3e3b1884c..00f63fbf80e06 100644 --- a/crates/anvil-polkadot/src/substrate_node/chain_spec.rs +++ b/crates/anvil-polkadot/src/substrate_node/chain_spec.rs @@ -1,11 +1,105 @@ +use crate::substrate_node::genesis::GenesisConfig; use polkadot_sdk::{ - sc_service::{self, ChainType, Properties}, + sc_chain_spec::{ChainSpec, GetExtension}, + sc_executor::HostFunctions, + sc_network::config::MultiaddrWithPeerId, + sc_service::{ChainType, GenericChainSpec, Properties}, + sc_telemetry::TelemetryEndpoints, + sp_core::storage::Storage, sp_genesis_builder, + sp_runtime::BuildStorage, }; use substrate_runtime::WASM_BINARY; -/// This is a specialization of the general Substrate ChainSpec type. -pub type ChainSpec = sc_service::GenericChainSpec; +/// This is a wrapper around the general Substrate ChainSpec type that allows manual changes to the +/// genesis block. +#[derive(Clone)] +pub struct DevelopmentChainSpec, EHF = ()> { + inner: GenericChainSpec, + genesis_config: GenesisConfig, +} + +impl BuildStorage for DevelopmentChainSpec +where + EHF: HostFunctions, + GenericChainSpec: BuildStorage, +{ + fn assimilate_storage(&self, storage: &mut Storage) -> Result<(), String> { + self.inner.assimilate_storage(storage)?; + storage.top.extend(self.genesis_config.as_storage_key_value()); + Ok(()) + } +} + +impl ChainSpec for DevelopmentChainSpec +where + E: GetExtension + serde::Serialize + Clone + Send + Sync + 'static, + EHF: HostFunctions, +{ + fn boot_nodes(&self) -> &[MultiaddrWithPeerId] { + self.inner.boot_nodes() + } + + fn name(&self) -> &str { + self.inner.name() + } + + fn id(&self) -> &str { + self.inner.id() + } + + fn chain_type(&self) -> ChainType { + self.inner.chain_type() + } + + fn telemetry_endpoints(&self) -> &Option { + self.inner.telemetry_endpoints() + } + + fn protocol_id(&self) -> Option<&str> { + self.inner.protocol_id() + } + + fn fork_id(&self) -> Option<&str> { + self.inner.fork_id() + } + + fn properties(&self) -> Properties { + self.inner.properties() + } + + fn add_boot_node(&mut self, addr: MultiaddrWithPeerId) { + self.inner.add_boot_node(addr) + } + + fn extensions(&self) -> &dyn GetExtension { + self.inner.extensions() as &dyn GetExtension + } + + fn extensions_mut(&mut self) -> &mut dyn GetExtension { + self.inner.extensions_mut() as &mut dyn GetExtension + } + + fn as_json(&self, raw: bool) -> Result { + self.inner.as_json(raw) + } + + fn as_storage_builder(&self) -> &dyn BuildStorage { + self + } + + fn cloned_box(&self) -> Box { + Box::new(Self { inner: self.inner.clone(), genesis_config: self.genesis_config.clone() }) + } + + fn set_storage(&mut self, storage: Storage) { + self.inner.set_storage(storage); + } + + fn code_substitutes(&self) -> std::collections::BTreeMap> { + self.inner.code_substitutes() + } +} fn props() -> Properties { let mut properties = Properties::new(); @@ -14,12 +108,18 @@ fn props() -> Properties { properties } -pub fn development_chain_spec() -> Result { - Ok(ChainSpec::builder(WASM_BINARY.expect("Development wasm not available"), Default::default()) - .with_name("Development") - .with_id("dev") - .with_chain_type(ChainType::Development) - .with_genesis_config_preset_name(sp_genesis_builder::DEV_RUNTIME_PRESET) - .with_properties(props()) - .build()) +pub fn development_chain_spec( + genesis_config: GenesisConfig, +) -> Result { + let inner = GenericChainSpec::builder( + WASM_BINARY.expect("Development wasm not available"), + Default::default(), + ) + .with_name("Development") + .with_id("dev") + .with_chain_type(ChainType::Development) + .with_genesis_config_preset_name(sp_genesis_builder::DEV_RUNTIME_PRESET) + .with_properties(props()) + .build(); + Ok(DevelopmentChainSpec { inner, genesis_config }) } diff --git a/crates/anvil-polkadot/src/substrate_node/genesis.rs b/crates/anvil-polkadot/src/substrate_node/genesis.rs new file mode 100644 index 0000000000000..0373adb723c5f --- /dev/null +++ b/crates/anvil-polkadot/src/substrate_node/genesis.rs @@ -0,0 +1,214 @@ +//! Genesis settings + +use crate::config::AnvilNodeConfig; +use alloy_genesis::GenesisAccount; +use alloy_primitives::Address; +use codec::Encode; +use polkadot_sdk::{ + sc_chain_spec::{BuildGenesisBlock, resolve_state_version_from_wasm}, + sc_client_api::{BlockImportOperation, backend::Backend}, + sc_executor::RuntimeVersionOf, + sp_blockchain, + sp_core::storage::Storage, + sp_runtime::{ + BuildStorage, + traits::{Block as BlockT, Hash as HashT, HashingFor, Header as HeaderT}, + }, +}; +use std::{collections::BTreeMap, marker::PhantomData, sync::Arc}; + +// Hex-encode key: 0x9527366927478e710d3f7fb77c6d1f89 +pub const CHAIN_ID_KEY: [u8; 16] = [ + 149u8, 39u8, 54u8, 105u8, 39u8, 71u8, 142u8, 113u8, 13u8, 63u8, 127u8, 183u8, 124u8, 109u8, + 31u8, 137u8, +]; + +// Hex-encode key: 0xf0c365c3cf59d671eb72da0e7a4113c49f1f0515f462cdcf84e0f1d6045dfcbb +// twox_128(b"Timestamp") ++ twox_128(b"Now") +// corresponds to `Timestamp::Now` storage item in pallet-timestamp +pub const TIMESTAMP_KEY: [u8; 32] = [ + 240u8, 195u8, 101u8, 195u8, 207u8, 89u8, 214u8, 113u8, 235u8, 114u8, 218u8, 14u8, 122u8, 65u8, + 19u8, 196u8, 159u8, 31u8, 5u8, 21u8, 244u8, 98u8, 205u8, 207u8, 132u8, 224u8, 241u8, 214u8, + 4u8, 93u8, 252u8, 187u8, +]; + +// Hex-encode key: 0x26aa394eea5630e07c48ae0c9558cef702a5c1b19ab7a04f536c519aca4983ac +// twox_128(b"System") ++ twox_128(b"Number") +// corresponds to `System::Number` storage item in pallet-system +pub const BLOCK_NUMBER_KEY: [u8; 32] = [ + 38u8, 170u8, 57u8, 78u8, 234u8, 86u8, 48u8, 224u8, 124u8, 72u8, 174u8, 12u8, 149u8, 88u8, + 206u8, 247u8, 2u8, 165u8, 193u8, 177u8, 154u8, 183u8, 160u8, 79u8, 83u8, 108u8, 81u8, 154u8, + 202u8, 73u8, 131u8, 172u8, +]; + +/// Genesis settings +#[derive(Clone, Debug, Default)] +pub struct GenesisConfig { + /// The chain id of the Substrate chain. + pub chain_id: u64, + /// The initial timestamp for the genesis block in milliseconds + pub timestamp: u64, + /// All accounts that should be initialised at genesis with their info. + pub alloc: Option>, + /// The initial number for the genesis block + pub number: u32, + /// The genesis header base fee + pub base_fee_per_gas: u64, + /// The genesis header gas limit. + pub gas_limit: Option, +} + +impl<'a> From<&'a AnvilNodeConfig> for GenesisConfig { + fn from(anvil_config: &'a AnvilNodeConfig) -> Self { + Self { + chain_id: anvil_config.get_chain_id(), + // Anvil genesis timestamp is in seconds, while Substrate timestamp is in milliseconds. + timestamp: anvil_config + .get_genesis_timestamp() + .checked_mul(1000) + .expect("Genesis timestamp overflow"), + alloc: anvil_config.genesis.as_ref().map(|g| g.alloc.clone()), + number: anvil_config + .get_genesis_number() + .try_into() + .expect("Genesis block number overflow"), + base_fee_per_gas: anvil_config.get_base_fee(), + gas_limit: anvil_config.gas_limit, + } + } +} + +impl GenesisConfig { + pub fn as_storage_key_value(&self) -> Vec<(Vec, Vec)> { + let storage = vec![ + (CHAIN_ID_KEY.to_vec(), self.chain_id.encode()), + (TIMESTAMP_KEY.to_vec(), self.timestamp.encode()), + (BLOCK_NUMBER_KEY.to_vec(), self.number.encode()), + ]; + // TODO: add other fields + storage + } +} + +pub struct DevelopmentGenesisBlockBuilder { + genesis_number: u32, + genesis_storage: Storage, + commit_genesis_state: bool, + backend: Arc, + executor: E, + _phantom: PhantomData, +} + +impl, E: RuntimeVersionOf> + DevelopmentGenesisBlockBuilder +{ + pub fn new( + genesis_number: u64, + build_genesis_storage: &dyn BuildStorage, + commit_genesis_state: bool, + backend: Arc, + executor: E, + ) -> sp_blockchain::Result { + let genesis_storage = + build_genesis_storage.build_storage().map_err(sp_blockchain::Error::Storage)?; + Self::new_with_storage( + genesis_number, + genesis_storage, + commit_genesis_state, + backend, + executor, + ) + } + + pub fn new_with_storage( + genesis_number: u64, + genesis_storage: Storage, + commit_genesis_state: bool, + backend: Arc, + executor: E, + ) -> sp_blockchain::Result { + Ok(Self { + genesis_number: genesis_number.try_into().map_err(|_| { + sp_blockchain::Error::Application( + format!( + "Genesis number {} is too large for u32 (max: {})", + genesis_number, + u32::MAX + ) + .into(), + ) + })?, + genesis_storage, + commit_genesis_state, + backend, + executor, + _phantom: PhantomData::, + }) + } +} + +impl, E: RuntimeVersionOf> BuildGenesisBlock + for DevelopmentGenesisBlockBuilder +{ + type BlockImportOperation = >::BlockImportOperation; + + fn build_genesis_block(self) -> sp_blockchain::Result<(Block, Self::BlockImportOperation)> { + let Self { + genesis_number, + genesis_storage, + commit_genesis_state, + backend, + executor, + _phantom, + } = self; + + let genesis_state_version = + resolve_state_version_from_wasm::<_, HashingFor>(&genesis_storage, &executor)?; + let mut op = backend.begin_operation()?; + let state_root = + op.set_genesis_state(genesis_storage, commit_genesis_state, genesis_state_version)?; + let extrinsics_root = <<::Header as HeaderT>::Hashing as HashT>::trie_root( + Vec::new(), + genesis_state_version, + ); + let genesis_block = Block::new( + <::Header as HeaderT>::new( + genesis_number.into(), + extrinsics_root, + state_root, + Default::default(), + Default::default(), + ), + Default::default(), + ); + + Ok((genesis_block, op)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_storage_encoding() { + let block_number: u32 = 5; + let timestamp: u64 = 10; + let chain_id: u64 = 42; + let genesis_config = + GenesisConfig { number: block_number, timestamp, chain_id, ..Default::default() }; + let genesis_storage = genesis_config.as_storage_key_value(); + assert!( + genesis_storage.contains(&(BLOCK_NUMBER_KEY.to_vec(), block_number.encode())), + "Block number not found in genesis key-value storage" + ); + assert!( + genesis_storage.contains(&(TIMESTAMP_KEY.to_vec(), timestamp.encode())), + "Timestamp not found in genesis key-value storage" + ); + assert!( + genesis_storage.contains(&(CHAIN_ID_KEY.to_vec(), chain_id.encode())), + "Chain id not found in genesis key-value storage" + ); + } +} diff --git a/crates/anvil-polkadot/src/substrate_node/mod.rs b/crates/anvil-polkadot/src/substrate_node/mod.rs index c8a973113eb8c..b6183a1be18b8 100644 --- a/crates/anvil-polkadot/src/substrate_node/mod.rs +++ b/crates/anvil-polkadot/src/substrate_node/mod.rs @@ -1,6 +1,8 @@ pub mod chain_spec; +pub mod genesis; pub mod host; pub mod impersonation; pub mod in_mem_rpc; pub mod mining_engine; +pub mod rpc; pub mod service; diff --git a/crates/anvil-polkadot/src/substrate_node/rpc.rs b/crates/anvil-polkadot/src/substrate_node/rpc.rs new file mode 100644 index 0000000000000..1ef605a25b795 --- /dev/null +++ b/crates/anvil-polkadot/src/substrate_node/rpc.rs @@ -0,0 +1,237 @@ +use crate::substrate_node::service::{Backend, FullClient}; +use jsonrpsee::RpcModule; +use polkadot_sdk::{ + sc_chain_spec::ChainSpec, + sc_client_api::{Backend as ClientBackend, HeaderBackend}, + sc_client_db::{BlocksPruning, PruningMode}, + sc_network_types::{self, multiaddr::Multiaddr}, + sc_rpc::{ + author::AuthorApiServer, + chain::ChainApiServer, + offchain::OffchainApiServer, + state::{ChildStateApiServer, StateApiServer}, + system::{Request, SystemApiServer, SystemInfo}, + }, + sc_rpc_api::DenyUnsafe, + sc_rpc_spec_v2::{ + archive::ArchiveApiServer, + chain_head::ChainHeadApiServer, + chain_spec::ChainSpecApiServer, + transaction::{TransactionApiServer, TransactionBroadcastApiServer}, + }, + sc_service::{ + self, Configuration, RpcHandlers, SpawnTaskHandle, TaskManager, + error::Error as ServiceError, + }, + sc_transaction_pool::TransactionPoolWrapper, + sc_utils::mpsc::{TracingUnboundedSender, tracing_unbounded}, + sp_keystore::KeystorePtr, + substrate_frame_rpc_system::SystemApiServer as _, +}; +use std::sync::Arc; +use substrate_runtime::OpaqueBlock as Block; + +pub fn spawn_rpc_server( + genesis_number: u64, + task_manager: &mut TaskManager, + client: Arc, + mut config: Configuration, + transaction_pool: Arc>, + keystore: KeystorePtr, + backend: Arc, +) -> Result { + let (system_rpc_tx, system_rpc_rx) = tracing_unbounded("mpsc_system_rpc", 10_000); + + let rpc_id_provider = config.rpc.id_provider.take(); + + let gen_rpc_module = || { + gen_rpc_module( + genesis_number, + task_manager.spawn_handle(), + client.clone(), + transaction_pool.clone(), + keystore.clone(), + system_rpc_tx.clone(), + config.impl_name.clone(), + config.impl_version.clone(), + config.chain_spec.as_ref(), + &config.state_pruning, + config.blocks_pruning, + backend.clone(), + ) + }; + + let rpc_server_handle = sc_service::start_rpc_servers( + &config.rpc, + config.prometheus_registry(), + &config.tokio_handle, + gen_rpc_module, + rpc_id_provider, + )?; + + let listen_addrs = rpc_server_handle + .listen_addrs() + .iter() + .map(|socket_addr| { + let mut multiaddr: Multiaddr = socket_addr.ip().into(); + multiaddr.push(sc_network_types::multiaddr::Protocol::Tcp(socket_addr.port())); + multiaddr + }) + .collect(); + + let in_memory_rpc = { + let mut module = gen_rpc_module()?; + module.extensions_mut().insert(DenyUnsafe::No); + module + }; + + let in_memory_rpc_handle = RpcHandlers::new(Arc::new(in_memory_rpc), listen_addrs); + + task_manager.keep_alive((config.base_path, rpc_server_handle, system_rpc_rx)); + + Ok(in_memory_rpc_handle) +} + +// Re-implement RPC module generation without the check on the genesis block number. +// The code is identical to the one in +// https://github.com/paritytech/polkadot-sdk/blob/9e0636567bebf312b065ca3acb285a8b32499df7/substrate/client/service/src/builder.rs#L754 +// apart from the creation of the RPC builder inside the function and the genesis number check. +#[allow(clippy::too_many_arguments)] +fn gen_rpc_module( + genesis_number: u64, + spawn_handle: SpawnTaskHandle, + client: Arc, + transaction_pool: Arc>, + keystore: KeystorePtr, + system_rpc_tx: TracingUnboundedSender>, + impl_name: String, + impl_version: String, + chain_spec: &dyn ChainSpec, + state_pruning: &Option, + blocks_pruning: BlocksPruning, + backend: Arc, +) -> Result, ServiceError> { + // Different from the original code, we create the RPC builder inside the function. + let rpc_builder = { + let client = client.clone(); + let pool = transaction_pool.clone(); + + Box::new(move |_| { + let rpc_builder_ext: Result<_, ServiceError> = Ok( + polkadot_sdk::substrate_frame_rpc_system::System::new(client.clone(), pool.clone()) + .into_rpc(), + ); + rpc_builder_ext + }) + }; + + let system_info = SystemInfo { + chain_name: chain_spec.name().into(), + impl_name, + impl_version, + properties: chain_spec.properties(), + chain_type: chain_spec.chain_type(), + }; + + let mut rpc_api = RpcModule::new(()); + let task_executor = Arc::new(spawn_handle); + + let (chain, state, child_state) = { + let chain = + polkadot_sdk::sc_rpc::chain::new_full(client.clone(), task_executor.clone()).into_rpc(); + let (state, child_state) = + polkadot_sdk::sc_rpc::state::new_full(client.clone(), task_executor.clone()); + let state = state.into_rpc(); + let child_state = child_state.into_rpc(); + + (chain, state, child_state) + }; + + const MAX_TRANSACTION_PER_CONNECTION: usize = 16; + + let transaction_broadcast_rpc_v2 = + polkadot_sdk::sc_rpc_spec_v2::transaction::TransactionBroadcast::new( + client.clone(), + transaction_pool.clone(), + task_executor.clone(), + MAX_TRANSACTION_PER_CONNECTION, + ) + .into_rpc(); + + let transaction_v2 = polkadot_sdk::sc_rpc_spec_v2::transaction::Transaction::new( + client.clone(), + transaction_pool.clone(), + task_executor.clone(), + None, + ) + .into_rpc(); + + let chain_head_v2 = polkadot_sdk::sc_rpc_spec_v2::chain_head::ChainHead::new( + client.clone(), + backend.clone(), + task_executor.clone(), + // Defaults to sensible limits for the `ChainHead`. + polkadot_sdk::sc_rpc_spec_v2::chain_head::ChainHeadConfig::default(), + ) + .into_rpc(); + + let is_archive_node = state_pruning.as_ref().map(|sp| sp.is_archive()).unwrap_or(false) + && blocks_pruning.is_archive(); + // Different from the original code, we use the genesis number to get the genesis hash. + let Some(genesis_hash) = client.hash(genesis_number as u32).ok().flatten() else { + return Err(ServiceError::Application( + format!("Genesis hash not found for genesis block number {genesis_number}").into(), + )); + }; + if is_archive_node { + let archive_v2 = polkadot_sdk::sc_rpc_spec_v2::archive::Archive::new( + client.clone(), + backend.clone(), + genesis_hash, + task_executor.clone(), + ) + .into_rpc(); + rpc_api.merge(archive_v2).map_err(|e| ServiceError::Application(e.into()))?; + } + + let chain_spec_v2 = polkadot_sdk::sc_rpc_spec_v2::chain_spec::ChainSpec::new( + chain_spec.name().into(), + genesis_hash, + chain_spec.properties(), + ) + .into_rpc(); + + let author = polkadot_sdk::sc_rpc::author::Author::new( + client, + transaction_pool, + keystore, + task_executor.clone(), + ) + .into_rpc(); + + let system = polkadot_sdk::sc_rpc::system::System::new(system_info, system_rpc_tx).into_rpc(); + + if let Some(storage) = backend.offchain_storage() { + let offchain = polkadot_sdk::sc_rpc::offchain::Offchain::new(storage).into_rpc(); + + rpc_api.merge(offchain).map_err(|e| ServiceError::Application(e.into()))?; + } + + // Part of the RPC v2 spec. + rpc_api.merge(transaction_v2).map_err(|e| ServiceError::Application(e.into()))?; + rpc_api.merge(transaction_broadcast_rpc_v2).map_err(|e| ServiceError::Application(e.into()))?; + rpc_api.merge(chain_head_v2).map_err(|e| ServiceError::Application(e.into()))?; + rpc_api.merge(chain_spec_v2).map_err(|e| ServiceError::Application(e.into()))?; + + // Part of the old RPC spec. + rpc_api.merge(chain).map_err(|e| ServiceError::Application(e.into()))?; + rpc_api.merge(author).map_err(|e| ServiceError::Application(e.into()))?; + rpc_api.merge(system).map_err(|e| ServiceError::Application(e.into()))?; + rpc_api.merge(state).map_err(|e| ServiceError::Application(e.into()))?; + rpc_api.merge(child_state).map_err(|e| ServiceError::Application(e.into()))?; + // Additional [`RpcModule`]s defined in the node to fit the specific blockchain + let extra_rpcs = rpc_builder(task_executor)?; + rpc_api.merge(extra_rpcs).map_err(|e| ServiceError::Application(e.into()))?; + + Ok(rpc_api) +} diff --git a/crates/anvil-polkadot/src/substrate_node/service.rs b/crates/anvil-polkadot/src/substrate_node/service.rs index fc7f88cc33c30..19081aa7ce19d 100644 --- a/crates/anvil-polkadot/src/substrate_node/service.rs +++ b/crates/anvil-polkadot/src/substrate_node/service.rs @@ -1,32 +1,27 @@ use crate::{ AnvilNodeConfig, - substrate_node::mining_engine::{MiningEngine, MiningMode, run_mining_engine}, + substrate_node::{ + genesis::DevelopmentGenesisBlockBuilder, + host::{PublicKeyToHashOverride, SenderAddressRecoveryOverride}, + mining_engine::{MiningEngine, MiningMode, run_mining_engine}, + rpc::spawn_rpc_server, + }, }; use anvil::eth::backend::time::TimeManager; use polkadot_sdk::{ - sc_basic_authorship, sc_consensus, sc_consensus_manual_seal, - sc_executor::WasmExecutor, - sc_network_types::{self, multiaddr::Multiaddr}, - sc_rpc_api::DenyUnsafe, + sc_basic_authorship, sc_consensus, sc_consensus_manual_seal, sc_executor, sc_service::{ self, Configuration, RpcHandlers, SpawnTaskHandle, TaskManager, error::Error as ServiceError, }, - sc_transaction_pool::{self, TransactionPoolWrapper}, - sc_utils::mpsc::tracing_unbounded, - sp_io, - sp_keystore::KeystorePtr, - sp_timestamp, + sc_transaction_pool, sp_io, sp_timestamp, sp_wasm_interface::ExtendedHostFunctions, - substrate_frame_rpc_system::SystemApiServer, }; use std::sync::Arc; use substrate_runtime::{OpaqueBlock as Block, RuntimeApi}; use tokio_stream::wrappers::ReceiverStream; -use crate::substrate_node::host::{PublicKeyToHashOverride, SenderAddressRecoveryOverride}; - -pub type Executor = WasmExecutor< +type Executor = sc_executor::WasmExecutor< ExtendedHostFunctions< ExtendedHostFunctions, PublicKeyToHashOverride, @@ -46,6 +41,7 @@ pub struct Service { pub tx_pool: Arc, pub rpc_handlers: RpcHandlers, pub mining_engine: Arc, + pub genesis_block_number: u64, } /// Builds a new service for a full client. @@ -53,11 +49,25 @@ pub fn new( anvil_config: &AnvilNodeConfig, config: Configuration, ) -> Result<(Service, TaskManager), ServiceError> { + let backend = sc_service::new_db_backend(config.db_config())?; + + let wasm_executor = sc_service::new_wasm_executor(&config.executor); + let genesis_block_builder = DevelopmentGenesisBlockBuilder::new( + anvil_config.get_genesis_number(), + config.chain_spec.as_storage_builder(), + !config.no_genesis(), + backend.clone(), + wasm_executor.clone(), + )?; + let (client, backend, keystore_container, mut task_manager) = - sc_service::new_full_parts::( + sc_service::new_full_parts_with_genesis_builder( &config, None, - sc_service::new_wasm_executor(&config.executor), + wasm_executor, + backend, + genesis_block_builder, + false, )?; let client = Arc::new(client); @@ -83,8 +93,15 @@ pub fn new( let mining_mode = MiningMode::new(anvil_config.block_time, anvil_config.mixed_mining, anvil_config.no_mining); - let time_manager = - Arc::new(TimeManager::new_with_milliseconds(sp_timestamp::Timestamp::current().into())); + let time_manager = Arc::new(TimeManager::new_with_milliseconds( + sp_timestamp::Timestamp::from( + anvil_config + .get_genesis_timestamp() + .checked_mul(1000) + .ok_or(ServiceError::Application("Genesis timestamp overflow".into()))?, + ) + .into(), + )); let mining_engine = Arc::new(MiningEngine::new( mining_mode, transaction_pool.clone(), @@ -93,6 +110,7 @@ pub fn new( )); let rpc_handlers = spawn_rpc_server( + anvil_config.get_genesis_number(), &mut task_manager, client.clone(), config, @@ -148,78 +166,8 @@ pub fn new( tx_pool: transaction_pool, rpc_handlers, mining_engine, + genesis_block_number: anvil_config.get_genesis_number(), }, task_manager, )) } - -fn spawn_rpc_server( - task_manager: &mut TaskManager, - client: Arc, - mut config: Configuration, - transaction_pool: Arc>, - keystore: KeystorePtr, - backend: Arc, -) -> Result { - let rpc_extensions_builder = { - let client = client.clone(); - let pool = transaction_pool.clone(); - - Box::new(move |_| { - Ok(polkadot_sdk::substrate_frame_rpc_system::System::new(client.clone(), pool.clone()) - .into_rpc()) - }) - }; - - let (system_rpc_tx, system_rpc_rx) = tracing_unbounded("mpsc_system_rpc", 10_000); - - let rpc_id_provider = config.rpc.id_provider.take(); - - let gen_rpc_module = || { - sc_service::gen_rpc_module( - task_manager.spawn_handle(), - client.clone(), - transaction_pool.clone(), - keystore.clone(), - system_rpc_tx.clone(), - config.impl_name.clone(), - config.impl_version.clone(), - config.chain_spec.as_ref(), - &config.state_pruning, - config.blocks_pruning, - backend.clone(), - &*rpc_extensions_builder, - None, - ) - }; - - let rpc_server_handle = sc_service::start_rpc_servers( - &config.rpc, - config.prometheus_registry(), - &config.tokio_handle, - gen_rpc_module, - rpc_id_provider, - )?; - - let listen_addrs = rpc_server_handle - .listen_addrs() - .iter() - .map(|socket_addr| { - let mut multiaddr: Multiaddr = socket_addr.ip().into(); - multiaddr.push(sc_network_types::multiaddr::Protocol::Tcp(socket_addr.port())); - multiaddr - }) - .collect(); - - let in_memory_rpc = { - let mut module = gen_rpc_module()?; - module.extensions_mut().insert(DenyUnsafe::No); - module - }; - - let in_memory_rpc_handle = RpcHandlers::new(Arc::new(in_memory_rpc), listen_addrs); - - task_manager.keep_alive((config.base_path, rpc_server_handle, system_rpc_rx)); - - Ok(in_memory_rpc_handle) -} diff --git a/crates/anvil-polkadot/tests/it/genesis.rs b/crates/anvil-polkadot/tests/it/genesis.rs new file mode 100644 index 0000000000000..90ee9091c63df --- /dev/null +++ b/crates/anvil-polkadot/tests/it/genesis.rs @@ -0,0 +1,45 @@ +use crate::utils::{TestNode, assert_with_tolerance, to_hex_string, unwrap_response}; +use alloy_primitives::U256; +use anvil_core::eth::EthRequest; +use anvil_polkadot::config::{AnvilNodeConfig, SubstrateNodeConfig}; + +#[tokio::test(flavor = "multi_thread")] +async fn test_genesis() { + let genesis_block_number: u32 = 1000; + let anvil_genesis_timestamp: u64 = 42; + let chain_id: u64 = 4242; + let anvil_node_config = AnvilNodeConfig::test_config() + .with_genesis_block_number(Some(genesis_block_number)) + .with_genesis_timestamp(Some(anvil_genesis_timestamp)) + .with_chain_id(Some(chain_id)); + let substrate_node_config = SubstrateNodeConfig::new(&anvil_node_config); + let mut node = TestNode::new(anvil_node_config, substrate_node_config).await.unwrap(); + + // Check that block number, timestamp, and chain id are set correctly at genesis + assert_eq!(node.best_block_number().await, genesis_block_number); + let genesis_hash = node.block_hash_by_number(genesis_block_number).await.unwrap(); + // Anvil genesis timestamp is in seconds, while Substrate timestamp is in milliseconds. + let genesis_timestamp = anvil_genesis_timestamp.checked_mul(1000).unwrap(); + let actual_genesis_timestamp = node.get_decoded_timestamp(Some(genesis_hash)).await; + assert_eq!(actual_genesis_timestamp, genesis_timestamp); + let current_chain_id_hex = + unwrap_response::(node.eth_rpc(EthRequest::EthChainId(())).await.unwrap()).unwrap(); + assert_eq!(current_chain_id_hex, to_hex_string(chain_id)); + + // Manually mine two blocks and force the timestamp to be increasing with 1 second each time. + unwrap_response::<()>( + node.eth_rpc(EthRequest::Mine(Some(U256::from(2)), Some(U256::from(1)))).await.unwrap(), + ) + .unwrap(); + + let latest_block_number = node.best_block_number().await; + assert_eq!(latest_block_number, genesis_block_number + 2); + let hash2 = node.block_hash_by_number(genesis_block_number + 2).await.unwrap(); + let timestamp2 = node.get_decoded_timestamp(Some(hash2)).await; + assert_with_tolerance( + timestamp2.saturating_sub(genesis_timestamp), + 2000, + 500, + "Timestamp is not increasing as expected from genesis.", + ); +} diff --git a/crates/anvil-polkadot/tests/it/main.rs b/crates/anvil-polkadot/tests/it/main.rs index e42f75e14a4b3..525c25124627b 100644 --- a/crates/anvil-polkadot/tests/it/main.rs +++ b/crates/anvil-polkadot/tests/it/main.rs @@ -1,4 +1,5 @@ mod abi; +mod genesis; mod impersonation; mod mining; mod standard_rpc; diff --git a/crates/anvil-polkadot/tests/it/standard_rpc.rs b/crates/anvil-polkadot/tests/it/standard_rpc.rs index 2314500cde5ad..64643948c8f43 100644 --- a/crates/anvil-polkadot/tests/it/standard_rpc.rs +++ b/crates/anvil-polkadot/tests/it/standard_rpc.rs @@ -20,15 +20,15 @@ async fn test_get_chain_id() { let substrate_node_config = SubstrateNodeConfig::new(&anvil_node_config); let mut node = TestNode::new(anvil_node_config.clone(), substrate_node_config).await.unwrap(); - // expected 420420420 + // expected 31337, default value from the Anvil config assert_eq!( unwrap_response::(node.eth_rpc(EthRequest::EthChainId(())).await.unwrap()).unwrap(), - "0x190f1b44" + "0x7a69" ); - // expected 420420420 + // expected 31337, default value from the Anvil config assert_eq!( unwrap_response::(node.eth_rpc(EthRequest::EthNetworkId(())).await.unwrap()).unwrap(), - 0x190f1b44 + 0x7a69 ); } diff --git a/crates/anvil-polkadot/tests/it/utils.rs b/crates/anvil-polkadot/tests/it/utils.rs index ae5ca025571da..33880df88e111 100644 --- a/crates/anvil-polkadot/tests/it/utils.rs +++ b/crates/anvil-polkadot/tests/it/utils.rs @@ -10,12 +10,12 @@ use anvil_polkadot::{ logging::LoggingManager, opts::SubstrateCli, spawn, - substrate_node::service::Service, + substrate_node::{genesis::GenesisConfig, service::Service}, }; use anvil_rpc::{error::RpcError, response::ResponseResult}; +use codec::Decode; use eyre::{Result, WrapErr}; use futures::{StreamExt, channel::oneshot}; -use parity_scale_codec::Decode; use polkadot_sdk::{ pallet_revive::evm::{Block, ReceiptInfo}, polkadot_sdk_frame::traits::Header, @@ -77,7 +77,7 @@ impl TestNode { Some(_) => {} } - let substrate_client = SubstrateCli {}; + let substrate_client = SubstrateCli { genesis_config: GenesisConfig::from(&anvil_config) }; let config = substrate_config.create_configuration(&substrate_client, handle.clone())?; let logging_manager = if anvil_config.enable_tracing { init_tracing(anvil_config.silent) @@ -300,3 +300,10 @@ pub fn get_contract_code(name: &str) -> ContractCode { ContractCode { init, runtime } } + +pub fn to_hex_string(value: u64) -> String { + let hex = hex::encode(value.to_be_bytes()); + let trimmed = hex.trim_start_matches('0'); + let result = if trimmed.is_empty() { "0" } else { trimmed }; + format!("0x{result}") +} diff --git a/deny.toml b/deny.toml index fc5612c7d54ec..eb4831947c607 100644 --- a/deny.toml +++ b/deny.toml @@ -271,6 +271,9 @@ exceptions = [ { allow = [ "GPL-3.0-or-later WITH Classpath-exception-2.0", ], name = "sc-network-common" }, # Polkadot SDK + { allow = [ + "GPL-3.0-or-later WITH Classpath-exception-2.0", + ], name = "sc-runtime-utilities" }, # Polkadot SDK ] # copyleft = "deny"