Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
6bc93a2
draft chainspec wrapper
re-gius Sep 9, 2025
38abb13
inject Anvil Node Config into Substrate CLI
re-gius Sep 10, 2025
9b33fb1
Set chain_id from config.json
re-gius Sep 11, 2025
30717d7
Add extra fields
re-gius Sep 15, 2025
66ee492
Merge branch 'master' into re-gius/chainspec-wrapper
re-gius Sep 15, 2025
85527e4
inject storage items into ChainSpec
re-gius Sep 16, 2025
b4420d0
implement ChainSpec trait for wrapper
re-gius Sep 17, 2025
b592f9a
Merge branch 'master' into re-gius/chainspec-wrapper
re-gius Sep 17, 2025
f807562
fix clippy
re-gius Sep 18, 2025
2ff0438
CR reorg
re-gius Sep 18, 2025
2302d04
Merge branch 'master' into re-gius/chainspec-wrapper
re-gius Sep 18, 2025
56b192f
Merge branch 'master' into re-gius/chainspec-wrapper
re-gius Sep 19, 2025
26e0fe7
set System::Number as u32
re-gius Sep 19, 2025
7caa8a5
nit
re-gius Sep 22, 2025
d513064
nit
re-gius Sep 22, 2025
5e80c89
Add genesis timestamp to time manager of mining node
re-gius Sep 22, 2025
7bafd06
Merge branch 'master' into re-gius/chainspec-wrapper
re-gius Sep 23, 2025
9d1a9d3
add custom genesis block number support in RPC
re-gius Sep 25, 2025
ce424ee
Merge branch 'master' into re-gius/chainspec-wrapper
re-gius Sep 25, 2025
16d17dc
fmt
re-gius Sep 25, 2025
0897f31
fmt
re-gius Sep 25, 2025
43e35d4
clippy + comments
re-gius Sep 26, 2025
329a139
Merge branch 'master' into re-gius/chainspec-wrapper
re-gius Sep 26, 2025
ce9de23
CR nits + reorg
re-gius Sep 26, 2025
6e67186
Add genesis integration tests (chain ID missing)
re-gius Sep 26, 2025
e2b2072
comment nit
re-gius Sep 29, 2025
3ee3857
add chain id test
re-gius Sep 29, 2025
3bd0395
Merge branch 'master' into re-gius/chainspec-wrapper
re-gius Sep 29, 2025
90da646
fix time manager creation
re-gius Sep 29, 2025
7221e82
changed genesis milliseconds mismatch + clippy for unused import
re-gius Sep 29, 2025
867d680
undo clippy change
re-gius Sep 29, 2025
327b973
Merge branch 'master' into re-gius/chainspec-wrapper
re-gius Sep 29, 2025
7680f00
merge fix - missing import
re-gius Sep 29, 2025
fcf200b
fix rpc client creation
re-gius Oct 1, 2025
5e42322
clippy nit
re-gius Oct 1, 2025
c51999b
fix
re-gius Oct 1, 2025
f8ba53b
Merge branch 'master' into re-gius/chainspec-wrapper
re-gius Oct 1, 2025
560962e
Merge branch 'master' into re-gius/chainspec-wrapper
re-gius Oct 7, 2025
e1abde2
CR fixes
re-gius Oct 8, 2025
c79e41b
fix types
re-gius Oct 8, 2025
c36ee17
nit
re-gius Oct 8, 2025
10b6b83
Use genesis block number to compute genesis hash
re-gius Oct 8, 2025
c5c7f47
improve error handling
re-gius Oct 8, 2025
4730f8e
fix metadata retrieval
alindima Oct 9, 2025
b3f7914
use the impersonation executor
alindima Oct 9, 2025
6a94b1a
use latest available metadata + error handling
re-gius Oct 9, 2025
8fa9e97
add Chain Id RPC
re-gius Oct 10, 2025
8a9689a
nit
re-gius Oct 10, 2025
11493c5
Merge branch 'master' into re-gius/chainspec-wrapper
re-gius Oct 10, 2025
5ae3b58
fmt + clippy
re-gius Oct 10, 2025
8915d00
add `InternalError` variant
re-gius Oct 10, 2025
e7cdd52
make test less flaky
re-gius Oct 10, 2025
986fd16
fix test with chain id RPC
re-gius Oct 10, 2025
a8ae12c
Use chain id RPC in genesis tests
re-gius Oct 10, 2025
88009ff
fix deny license
re-gius Oct 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion crates/anvil-polkadot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
polkadot-sdk = { version = "2507.1.0", default-features = false, features = [
"sc-allocator",
Expand All @@ -34,13 +35,15 @@ polkadot-sdk = { version = "2507.1.0", default-features = false, features = [
"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-service",
"sc-state-db",
"sc-telemetry",
"sc-tracing",
"sc-transaction-pool",
"sc-transaction-pool-api",
Expand Down Expand Up @@ -128,10 +131,10 @@ clap = { version = "4", features = [
clap_complete = { version = "4" }
chrono.workspace = true
clap_complete_fig = "4"
parity-scale-codec = "3.7.5"
subxt = "0.41.0"
subxt-signer = "0.41.0"
tokio-stream = "0.1.17"
sp-io = "42.0.0"

[dev-dependencies]
alloy-provider = { workspace = true, features = ["txpool-api"] }
Expand Down
102 changes: 102 additions & 0 deletions crates/anvil-polkadot/src/genesis.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//! Genesis settings

use crate::config::AnvilNodeConfig;
use alloy_genesis::GenesisAccount;
use alloy_primitives::Address;
use codec::Encode;
use std::collections::BTreeMap;

// 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
pub timestamp: u64,
/// The genesis block author address.
pub coinbase: Option<Address>,
/// All accounts that should be initialised at genesis with their info.
pub alloc: Option<BTreeMap<Address, GenesisAccount>>,
/// The initial number for the genesis block
pub number: u64,
/// The genesis header base fee
pub base_fee_per_gas: u64,
/// The genesis header gas limit.
pub gas_limit: Option<u128>,
}

impl From<AnvilNodeConfig> for GenesisConfig {
fn from(anvil_config: AnvilNodeConfig) -> Self {
Self {
chain_id: anvil_config.get_chain_id(),
timestamp: anvil_config.get_genesis_timestamp(),
coinbase: anvil_config.genesis.as_ref().map(|g| g.coinbase),
alloc: anvil_config.genesis.as_ref().map(|g| g.alloc.clone()),
number: anvil_config.get_genesis_number(),
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<u8>, Vec<u8>)> {
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
}
}

#[cfg(test)]
mod tests {
use super::*;
use sp_io::hashing::twox_128;

#[test]
fn test_number_storage_key() {
let system_hash = twox_128(b"System");
let number_hash = twox_128(b"Number");
let mut concatenated_number_hash = [0u8; 32];
concatenated_number_hash[..16].copy_from_slice(&system_hash);
concatenated_number_hash[16..].copy_from_slice(&number_hash);
assert_eq!(BLOCK_NUMBER_KEY, concatenated_number_hash);
}

#[test]
fn test_timestamp_storage_key() {
let timestamp_hash = twox_128(b"Timestamp");
let now_hash = twox_128(b"Now");
let mut concatenated_timestamp_hash = [0u8; 32];
concatenated_timestamp_hash[..16].copy_from_slice(&timestamp_hash);
concatenated_timestamp_hash[16..].copy_from_slice(&now_hash);
assert_eq!(TIMESTAMP_KEY, concatenated_timestamp_hash);
}
}
5 changes: 4 additions & 1 deletion crates/anvil-polkadot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub mod cmd;

pub mod opts;

pub mod genesis;

#[macro_use]
extern crate tracing;

Expand Down Expand Up @@ -83,10 +85,11 @@ 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 { anvil_config: anvil_config.clone() };

let tokio_runtime = build_runtime()?;

let signals = tokio_runtime.block_on(async { sc_cli::Signals::capture() })?;
Expand Down
15 changes: 9 additions & 6 deletions crates/anvil-polkadot/src/opts.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{cmd::NodeArgs, substrate_node::chain_spec};
use crate::{cmd::NodeArgs, config::AnvilNodeConfig, substrate_node::chain_spec};
use clap::{Parser, Subcommand};
use foundry_cli::opts::GlobalArgs;
use foundry_common::version::{LONG_VERSION, SHORT_VERSION};
Expand Down Expand Up @@ -31,7 +31,10 @@ pub enum AnvilSubcommand {
GenerateFigSpec,
}

pub struct SubstrateCli;
pub struct SubstrateCli {
// Used to inject the anvil config into the chain spec
pub anvil_config: AnvilNodeConfig,
}

// Implementation of the SubstrateCli, which enables us to launch an in-process substrate node.
impl sc_cli::SubstrateCli for SubstrateCli {
Expand Down Expand Up @@ -65,10 +68,10 @@ impl sc_cli::SubstrateCli for SubstrateCli {

fn load_spec(&self, id: &str) -> std::result::Result<Box<dyn sc_service::ChainSpec>, 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))?)
}
"dev" | "" => Box::new(chain_spec::development_chain_spec(self.anvil_config.clone())?),
path => Box::new(chain_spec::DevelopmentChainSpec::<Option<()>, ()>::from_json_file(
std::path::PathBuf::from(path),
)?),
})
}
}
144 changes: 133 additions & 11 deletions crates/anvil-polkadot/src/substrate_node/chain_spec.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,127 @@
use crate::{config::AnvilNodeConfig, 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<E = Option<()>, EHF = ()> {
inner: GenericChainSpec<E, EHF>,
genesis_config: GenesisConfig,
}

impl<E, EHF> BuildStorage for DevelopmentChainSpec<E, EHF>
where
EHF: HostFunctions,
GenericChainSpec<E, EHF>: 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(())
}
}

// Inherit all methods defined on GenericChainSpec.
impl<E, EHF> std::ops::Deref for DevelopmentChainSpec<E, EHF> {
type Target = GenericChainSpec<E, EHF>;

fn deref(&self) -> &Self::Target {
&self.inner
}
}

impl<E, EHF> std::ops::DerefMut for DevelopmentChainSpec<E, EHF> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}

impl<E, EHF> ChainSpec for DevelopmentChainSpec<E, EHF>
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<TelemetryEndpoints> {
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<String, String> {
self.inner.as_json(raw)
}

fn as_storage_builder(&self) -> &dyn BuildStorage {
self
}

fn cloned_box(&self) -> Box<dyn ChainSpec> {
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<String, Vec<u8>> {
self.inner.code_substitutes()
}
}

impl<E: serde::de::DeserializeOwned, EHF> DevelopmentChainSpec<E, EHF> {
pub fn from_json_file(path: std::path::PathBuf) -> Result<Self, String> {
let inner = GenericChainSpec::from_json_file(path)?;
Ok(Self { inner, genesis_config: GenesisConfig::default() })
}
}

fn props() -> Properties {
let mut properties = Properties::new();
Expand All @@ -14,12 +130,18 @@ fn props() -> Properties {
properties
}

pub fn development_chain_spec() -> Result<ChainSpec, String> {
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(
anvil_config: AnvilNodeConfig,
) -> Result<DevelopmentChainSpec, String> {
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: GenesisConfig::from(anvil_config) })
}
4 changes: 2 additions & 2 deletions crates/anvil-polkadot/tests/it/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ use anvil_polkadot::{
substrate_node::service::Service,
};
use anvil_rpc::response::ResponseResult;
use codec::Decode;
use eyre::{Result, WrapErr};
use futures::{channel::oneshot, StreamExt};
use parity_scale_codec::Decode;
use polkadot_sdk::{
pallet_revive_eth_rpc::subxt_client::{self, system::calls::types::Remark},
polkadot_sdk_frame::traits::Header,
Expand Down Expand Up @@ -60,7 +60,7 @@ impl TestNode {
Some(_) => {}
}

let substrate_client = SubstrateCli {};
let substrate_client = SubstrateCli { anvil_config: anvil_config.clone() };
let config = substrate_config.create_configuration(&substrate_client, handle.clone())?;
let (service, api) = spawn(anvil_config, config, LoggingManager::default()).await?;

Expand Down
Loading