From a52909422aa3fdfca163cce980f1ced51ced1479 Mon Sep 17 00:00:00 2001 From: Amar Singh Date: Thu, 21 Aug 2025 08:50:58 -0400 Subject: [PATCH] Polkadot Omni Node (#418) * add to link to omni node in the docs * clean and test both templates locally * add genesis config presets to runtime * add genesis config presets to evm template and toml sort generic template * update abstractions to use runtime genesis presets * runtime tests for genesis config presets for both templates * H160 type does not require clone so satisfy clippy --- .../ROOT/pages/guides/quick_start.adoc | 5 + evm-template/Cargo.lock | 7 +- evm-template/Cargo.toml | 4 +- evm-template/runtime/Cargo.toml | 8 +- .../runtime/src/genesis_config_presets.rs | 318 ++++++++++++++++++ evm-template/runtime/src/lib.rs | 2 + generic-template/Cargo.lock | 7 +- generic-template/Cargo.toml | 4 +- generic-template/runtime/Cargo.toml | 5 + .../runtime/src/genesis_config_presets.rs | 232 +++++++++++++ generic-template/runtime/src/lib.rs | 2 + 11 files changed, 586 insertions(+), 8 deletions(-) create mode 100644 evm-template/runtime/src/genesis_config_presets.rs create mode 100644 generic-template/runtime/src/genesis_config_presets.rs diff --git a/docs/modules/ROOT/pages/guides/quick_start.adoc b/docs/modules/ROOT/pages/guides/quick_start.adoc index f629415..c9efda1 100644 --- a/docs/modules/ROOT/pages/guides/quick_start.adoc +++ b/docs/modules/ROOT/pages/guides/quick_start.adoc @@ -55,6 +55,11 @@ We use the `generic-template-node` executable throughout all the commands since ./target/release/generic-template-node build-spec --chain plain-parachain-chainspec.json --disable-default-bootnode --raw > raw-parachain-chainspec.json ``` +[NOTE] +==== +At this point, you are free to use the omni-node. The command for using omni-node takes the form: `polkadot-omni-node --chain `. For more information about using omni-node, see the link:https://docs.polkadot.com/develop/toolkit/parachains/polkadot-omni-node/[polkadot-omni-node documentation]. +==== + * Run two nodes and wait until it syncs with the Paseo relay chain. This can take a fairly long time(up to 2 days), so we can use the `fast-unsafe` flag to make the process faster since we are on a testnet(~ 3 hours). `fast` downloads the blocks without executing the transactions, and `unsafe` skips downloading the state proofs(which we are ok with since it is a testnet). + ```bash diff --git a/evm-template/Cargo.lock b/evm-template/Cargo.lock index 8623a20..605dca9 100644 --- a/evm-template/Cargo.lock +++ b/evm-template/Cargo.lock @@ -3598,6 +3598,7 @@ dependencies = [ "cumulus-primitives-core", "cumulus-primitives-timestamp", "cumulus-primitives-utility", + "docify", "dp-consensus", "ethereum 0.15.0 (git+https://github.com/rust-ethereum/ethereum?rev=bbb544622208ef6e9890a2dbc224248f6dd13318)", "fp-account", @@ -3664,6 +3665,7 @@ dependencies = [ "polkadot-runtime-common", "polkadot-runtime-parachains", "scale-info", + "serde_json", "smallvec", "sp-api", "sp-arithmetic", @@ -3673,6 +3675,7 @@ dependencies = [ "sp-genesis-builder", "sp-inherents", "sp-io", + "sp-keyring", "sp-offchain", "sp-runtime", "sp-session", @@ -7786,7 +7789,7 @@ dependencies = [ [[package]] name = "openzeppelin-pallet-abstractions" version = "0.1.0" -source = "git+https://github.com/OpenZeppelin/openzeppelin-pallet-abstractions?branch=polkadot-stable2503#f4488330f19b575e54a4080b55a3d66a9fb4f702" +source = "git+https://github.com/OpenZeppelin/openzeppelin-pallet-abstractions?branch=polkadot-stable2503#bc6dcabdc7b66f218c39c0f3496710c7b81a4a09" dependencies = [ "cumulus-primitives-core", "frame-support", @@ -7799,7 +7802,7 @@ dependencies = [ [[package]] name = "openzeppelin-pallet-abstractions-proc" version = "0.1.0" -source = "git+https://github.com/OpenZeppelin/openzeppelin-pallet-abstractions?branch=polkadot-stable2503#f4488330f19b575e54a4080b55a3d66a9fb4f702" +source = "git+https://github.com/OpenZeppelin/openzeppelin-pallet-abstractions?branch=polkadot-stable2503#bc6dcabdc7b66f218c39c0f3496710c7b81a4a09" dependencies = [ "darling", "openzeppelin-pallet-abstractions", diff --git a/evm-template/Cargo.toml b/evm-template/Cargo.toml index ef794b8..2ac1c5f 100644 --- a/evm-template/Cargo.toml +++ b/evm-template/Cargo.toml @@ -12,6 +12,7 @@ repository = "https://github.com/OpenZeppelin/polkadot-runtime-templates" [workspace.dependencies] clap = { version = "4.5.3", features = [ "derive" ] } color-print = "0.3.4" +docify = { version = "0.2.9" } futures = "0.3.30" hex = "0.4.3" hex-literal = "0.4.1" @@ -24,7 +25,7 @@ parity-scale-codec = { version = "3.6.12", default-features = false, features = scale-info = { version = "2.11.1", default-features = false } serde = { version = "1.0.197", default-features = false } serde_derive = { version = "1.0.121", default-features = false } -serde_json = "1.0.121" +serde_json = { version = "1.0.121", default-features = false } smallvec = "1.11.0" frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } @@ -87,6 +88,7 @@ sp-core = { git = "https://github.com/paritytech/polkadot-sdk", default-features sp-genesis-builder = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } sp-inherents = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } sp-io = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } +sp-keyring = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } sp-keystore = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } sp-offchain = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } diff --git a/evm-template/runtime/Cargo.toml b/evm-template/runtime/Cargo.toml index e21970a..f6317cb 100644 --- a/evm-template/runtime/Cargo.toml +++ b/evm-template/runtime/Cargo.toml @@ -11,10 +11,12 @@ version = "3.0.0" targets = [ "x86_64-unknown-linux-gnu" ] [dependencies] -hex-literal = { workspace = true, optional = true } +docify = { workspace = true } +hex-literal = { version = "0.4.1", default-features = false } log = { workspace = true } parity-scale-codec = { workspace = true, features = [ "derive" ] } scale-info = { workspace = true, features = [ "derive" ] } +serde_json = { workspace = true, default-features = false, features = [ "alloc" ] } smallvec = { workspace = true } openzeppelin-pallet-abstractions = { workspace = true } @@ -57,6 +59,7 @@ sp-consensus-aura = { workspace = true } sp-core = { workspace = true } sp-genesis-builder = { workspace = true } sp-inherents = { workspace = true } +sp-keyring = { workspace = true } sp-offchain = { workspace = true } sp-runtime = { workspace = true } sp-session = { workspace = true } @@ -203,6 +206,7 @@ std = [ "polkadot-parachain-primitives/std", "polkadot-runtime-common/std", "scale-info/std", + "serde_json/std", "sp-api/std", "sp-arithmetic/std", "sp-block-builder/std", @@ -210,6 +214,7 @@ std = [ "sp-core/std", "sp-genesis-builder/std", "sp-inherents/std", + "sp-keyring/std", "sp-offchain/std", "sp-runtime/std", "sp-session/std", @@ -234,7 +239,6 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", - "hex-literal", "nimbus-primitives/runtime-benchmarks", "orml-oracle/runtime-benchmarks", "pallet-asset-manager/runtime-benchmarks", diff --git a/evm-template/runtime/src/genesis_config_presets.rs b/evm-template/runtime/src/genesis_config_presets.rs new file mode 100644 index 0000000..5a521dd --- /dev/null +++ b/evm-template/runtime/src/genesis_config_presets.rs @@ -0,0 +1,318 @@ +//! Genesis presets adapted for the EVM template (AccountId20 / H160). + +use alloc::{format, vec, vec::Vec}; + +use cumulus_primitives_core::ParaId; +use frame_support::build_struct_json_patch; +use parachains_common::AuraId; +use serde_json::Value; +use sp_core::{ecdsa, Pair, Public}; +use sp_genesis_builder::PresetId; +use sp_runtime::traits::{IdentifyAccount, Verify}; + +use crate::{ + constants::currency::EXISTENTIAL_DEPOSIT, AccountId, BalancesConfig, CollatorSelectionConfig, + ParachainInfoConfig, PolkadotXcmConfig, RuntimeGenesisConfig, SessionConfig, SessionKeys, + Signature, SudoConfig, +}; + +/// The default XCM version to set in genesis config. +const SAFE_XCM_VERSION: u32 = xcm::prelude::XCM_VERSION; + +/// Parachain id used for genesis config presets of parachain template. +#[docify::export_content] +pub const PARACHAIN_ID: u32 = 1000; + +/// Generate the session keys from individual elements. +pub fn template_session_keys(keys: AuraId) -> SessionKeys { + SessionKeys { aura: keys } +} + +/* ---------- Helpers brought over from node/chain_spec.rs ---------- */ + +/// Helper: produce a public key from seed (e.g., "Alice", "Bob"). +fn get_from_seed(seed: &str) -> ::Public { + TPublic::Pair::from_string(&format!("//{}", seed), None) + .expect("static values are valid; qed") + .public() +} + +/// Alias to the runtime’s MultiSignature signer type. +type AccountPublic = ::Signer; + +/// Helper: turn a seed into an AccountId20 (Ethereum-style address). +fn get_account_id_from_seed(seed: &str) -> AccountId +where + AccountPublic: From<::Public>, +{ + AccountPublic::from(get_from_seed::(seed)).into_account() +} + +/// Helper: collator session key (Aura) from seed. +fn get_collator_keys_from_seed(seed: &str) -> AuraId { + get_from_seed::(seed) +} + +/* ---------- End helpers ---------- */ + +fn testnet_genesis( + invulnerables: Vec<(AccountId, AuraId)>, + endowed_accounts: Vec, + root: AccountId, + id: ParaId, +) -> Value { + build_struct_json_patch!(RuntimeGenesisConfig { + balances: BalancesConfig { + balances: endowed_accounts + .iter() + .cloned() + .map(|k| (k, 1u128 << 60)) + .collect::>(), + }, + parachain_info: ParachainInfoConfig { parachain_id: id }, + collator_selection: CollatorSelectionConfig { + invulnerables: invulnerables.iter().cloned().map(|(acc, _)| acc).collect::>(), + candidacy_bond: EXISTENTIAL_DEPOSIT * 16, + }, + session: SessionConfig { + keys: invulnerables + .into_iter() + .map(|(acc, aura)| { + ( + acc, // account id (AccountId20) + acc, // validator id (AccountId20) + template_session_keys(aura), // session keys + ) + }) + .collect::>(), + }, + polkadot_xcm: PolkadotXcmConfig { safe_xcm_version: Some(SAFE_XCM_VERSION) }, + sudo: SudoConfig { key: Some(root) }, + }) +} + +fn local_testnet_genesis() -> Value { + use hex_literal::hex; + + // Collators: use ECDSA seeds -> AccountId20, and Aura keys from seed. + let invulnerables = vec![ + (get_account_id_from_seed::("Alice"), get_collator_keys_from_seed("Alice")), + (get_account_id_from_seed::("Bob"), get_collator_keys_from_seed("Bob")), + ]; + + // Endowed accounts: the standard EVM dev addresses (Alith, Baltathar, Charleth, Dorothy, Ethan), + // matching the node chain spec you pasted. + let endowed_accounts = vec![ + AccountId::from(hex!("f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac")), + AccountId::from(hex!("3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0")), + AccountId::from(hex!("798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc")), + AccountId::from(hex!("773539d4Ac0e786233D90A233654ccEE26a613D9")), + AccountId::from(hex!("Ff64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB")), + ]; + + // Sudo: Alith. + let root = AccountId::from(hex!("f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac")); + + testnet_genesis(invulnerables, endowed_accounts, root, PARACHAIN_ID.into()) +} + +fn development_config_genesis() -> Value { + // Mirror local_testnet for dev. + local_testnet_genesis() +} + +/// Provides the JSON representation of predefined genesis config for given `id`. +pub fn get_preset(id: &PresetId) -> Option> { + let patch = match id.as_ref() { + sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET => local_testnet_genesis(), + sp_genesis_builder::DEV_RUNTIME_PRESET => development_config_genesis(), + _ => return None, + }; + Some( + serde_json::to_string(&patch) + .expect("serialization to json is expected to work. qed.") + .into_bytes(), + ) +} + +/// List of supported presets. +pub fn preset_names() -> Vec { + vec![ + PresetId::from(sp_genesis_builder::DEV_RUNTIME_PRESET), + PresetId::from(sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET), + ] +} + +#[cfg(test)] +mod tests { + // We use hex! only inside tests. The runtime already depends on hex-literal for this file. + use hex_literal::hex; + use serde_json::json; + + use super::*; + + /// Parse a preset bytes blob with serde_json. + fn parse_bytes(b: Vec) -> Value { + serde_json::from_slice::(&b).expect("preset must be valid JSON") + } + + /// Serialize a runtime AccountId (AccountId20/H160) into JSON for comparison. + fn account_json(acc: &AccountId) -> Value { + serde_json::to_value(acc).expect("AccountId must serialize to JSON") + } + + #[test] + fn local_testnet_genesis_shape_and_contents() { + let v = super::local_testnet_genesis(); + + // Top-level keys present + for k in + ["balances", "parachainInfo", "collatorSelection", "session", "polkadotXcm", "sudo"] + { + assert!(v.get(k).is_some(), "{k} missing"); + } + + // ParachainId + assert_eq!(v["parachainInfo"]["parachainId"], json!(PARACHAIN_ID), "wrong parachain id"); + + // XCM version + assert_eq!( + v["polkadotXcm"]["safeXcmVersion"], + json!(Some(SAFE_XCM_VERSION)), + "wrong SAFE_XCM_VERSION" + ); + + // ----- Collators / invulnerables: must be [Alice, Bob] derived via ECDSA seeds ----- + let expected_alice = get_account_id_from_seed::("Alice"); + let expected_bob = get_account_id_from_seed::("Bob"); + let invuls = v["collatorSelection"]["invulnerables"] + .as_array() + .expect("collatorSelection.invulnerables must be an array"); + assert_eq!(invuls.len(), 2, "expected two invulnerables"); + assert_eq!( + invuls[0], + account_json(&expected_alice), + "first invulnerable must be Alice (ECDSA)" + ); + assert_eq!( + invuls[1], + account_json(&expected_bob), + "second invulnerable must be Bob (ECDSA)" + ); + + // candidacyBond must be EXISTENTIAL_DEPOSIT * 16 + assert_eq!( + v["collatorSelection"]["candidacyBond"], + json!(EXISTENTIAL_DEPOSIT * 16), + "wrong candidacy bond" + ); + + // ----- Session keys: one per invulnerable; controller == validator; contains 'aura' ----- + let sess = v["session"]["keys"].as_array().expect("session.keys must be an array"); + assert_eq!(sess.len(), invuls.len(), "session keys length must equal invulnerables length"); + for entry in sess { + let arr = entry + .as_array() + .expect("each session entry must be a 3-tuple [acc, validator, keys]"); + assert_eq!(arr.len(), 3, "session entry must have 3 elements"); + assert_eq!(arr[0], arr[1], "controller and validator account must match"); + assert!(arr[2].get("aura").is_some(), "session keys must contain 'aura'"); + } + + // ----- Sudo key is Alith (EVM dev addr) ----- + let alith = AccountId::from(hex!("f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac")); + assert_eq!(v["sudo"]["key"], json!(Some(account_json(&alith))), "sudo key must be Alith"); + + // ----- Endowed accounts: the 5 standard EVM dev addrs ----- + let expected_endowed: Vec = vec![ + AccountId::from(hex!("f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac")), // Alith + AccountId::from(hex!("3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0")), // Baltathar + AccountId::from(hex!("798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc")), // Charleth + AccountId::from(hex!("773539d4Ac0e786233D90A233654ccEE26a613D9")), // Dorothy + AccountId::from(hex!("Ff64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB")), // Ethan + ]; + + let balances = + v["balances"]["balances"].as_array().expect("balances.balances must be an array"); + + // Length check + assert_eq!(balances.len(), expected_endowed.len(), "endowed accounts length mismatch"); + + // Helper: turn a JSON account value (usually a hex string) into a comparable String. + fn json_acc_to_string(val: &Value) -> String { + // Expect string form (e.g., "0xf24f..."). If not, fall back to JSON rendering. + val.as_str().map(|s| s.to_owned()).unwrap_or_else(|| val.to_string()) + } + + // Extract the set of endowed accounts from the JSON + use std::collections::BTreeSet; + let got: BTreeSet = balances + .iter() + .map(|pair| { + let arr = pair.as_array().expect("[account, amount] pair"); + assert_eq!(arr.len(), 2, "balance entry must be [account, amount]"); + json_acc_to_string(&arr[0]) + }) + .collect(); + + // Build the expected set from our AccountId list + let want: BTreeSet = + expected_endowed.iter().map(|a| json_acc_to_string(&account_json(a))).collect(); + + assert_eq!(got, want, "endowed account set mismatch"); + } + + #[test] + fn development_genesis_mirrors_local_testnet() { + // In this template, development == local_testnet (per implementation). + let dev = super::development_config_genesis(); + let local = super::local_testnet_genesis(); + assert_eq!(dev, local, "development config should mirror local_testnet"); + } + + #[test] + fn get_preset_roundtrips_known_ids() { + // LOCAL_TESTNET + let p = + super::get_preset(&PresetId::from(sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET)) + .expect("local preset should exist"); + let from_api = parse_bytes(p); + let from_fn = super::local_testnet_genesis(); + assert_eq!(from_api, from_fn, "local_testnet preset must match function output"); + + // DEV + let p = super::get_preset(&PresetId::from(sp_genesis_builder::DEV_RUNTIME_PRESET)) + .expect("dev preset should exist"); + let from_api = parse_bytes(p); + let from_fn = super::development_config_genesis(); + assert_eq!(from_api, from_fn, "dev preset must match function output"); + } + + #[test] + fn preset_names_lists_supported() { + let names = super::preset_names(); + assert!( + names.contains(&PresetId::from(sp_genesis_builder::DEV_RUNTIME_PRESET)), + "DEV preset should be listed" + ); + assert!( + names.contains(&PresetId::from(sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET)), + "LOCAL_TESTNET preset should be listed" + ); + } + + #[test] + fn invulnerables_match_ecdsa_seed_derivation() { + // Ensure the addresses inside the JSON exactly match the ones derived from ECDSA seeds. + let v = super::local_testnet_genesis(); + let invuls = v["collatorSelection"]["invulnerables"] + .as_array() + .expect("collatorSelection.invulnerables must be an array"); + + let alice = get_account_id_from_seed::("Alice"); + let bob = get_account_id_from_seed::("Bob"); + + assert_eq!(invuls[0], account_json(&alice), "Alice derivation mismatch"); + assert_eq!(invuls[1], account_json(&bob), "Bob derivation mismatch"); + } +} diff --git a/evm-template/runtime/src/lib.rs b/evm-template/runtime/src/lib.rs index 4ef9310..ed52f79 100644 --- a/evm-template/runtime/src/lib.rs +++ b/evm-template/runtime/src/lib.rs @@ -12,6 +12,7 @@ pub mod configs; pub mod constants; mod precompiles; pub use precompiles::OpenZeppelinPrecompiles; +mod genesis_config_presets; mod types; mod weights; @@ -19,6 +20,7 @@ use frame_support::{ traits::OnFinalize, weights::{WeightToFeeCoefficient, WeightToFeeCoefficients, WeightToFeePolynomial}, }; +pub use genesis_config_presets::PARACHAIN_ID; use smallvec::smallvec; pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::H160; diff --git a/generic-template/Cargo.lock b/generic-template/Cargo.lock index c877036..09322ad 100644 --- a/generic-template/Cargo.lock +++ b/generic-template/Cargo.lock @@ -4234,6 +4234,7 @@ dependencies = [ "cumulus-primitives-core", "cumulus-primitives-timestamp", "cumulus-primitives-utility", + "docify", "dp-consensus", "frame-benchmarking", "frame-executive", @@ -4287,6 +4288,7 @@ dependencies = [ "polkadot-runtime-common", "polkadot-runtime-parachains", "scale-info", + "serde_json", "smallvec", "sp-api", "sp-arithmetic", @@ -4296,6 +4298,7 @@ dependencies = [ "sp-genesis-builder", "sp-inherents", "sp-io", + "sp-keyring", "sp-offchain", "sp-runtime", "sp-session", @@ -7170,7 +7173,7 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openzeppelin-pallet-abstractions" version = "0.1.0" -source = "git+https://github.com/OpenZeppelin/openzeppelin-pallet-abstractions?branch=polkadot-stable2503#971a66b7513adbbd1c51fcf74a3f85bf102f7bea" +source = "git+https://github.com/OpenZeppelin/openzeppelin-pallet-abstractions?branch=polkadot-stable2503#bc6dcabdc7b66f218c39c0f3496710c7b81a4a09" dependencies = [ "cumulus-primitives-core", "frame-support", @@ -7183,7 +7186,7 @@ dependencies = [ [[package]] name = "openzeppelin-pallet-abstractions-proc" version = "0.1.0" -source = "git+https://github.com/OpenZeppelin/openzeppelin-pallet-abstractions?branch=polkadot-stable2503#971a66b7513adbbd1c51fcf74a3f85bf102f7bea" +source = "git+https://github.com/OpenZeppelin/openzeppelin-pallet-abstractions?branch=polkadot-stable2503#bc6dcabdc7b66f218c39c0f3496710c7b81a4a09" dependencies = [ "darling", "openzeppelin-pallet-abstractions", diff --git a/generic-template/Cargo.toml b/generic-template/Cargo.toml index 062d3c2..f0d506e 100644 --- a/generic-template/Cargo.toml +++ b/generic-template/Cargo.toml @@ -12,6 +12,7 @@ repository = "https://github.com/OpenZeppelin/polkadot-runtime-templates" [workspace.dependencies] clap = { version = "4.5.3", features = [ "derive" ] } color-print = "0.3.4" +docify = { version = "0.2.9" } futures = "0.3.30" hex-literal = "0.4.1" jsonrpsee = { version = "0.24.3", features = [ "server" ] } @@ -22,7 +23,7 @@ parity-scale-codec = { version = "3.6.12", default-features = false, features = ] } scale-info = { version = "2.11.1", default-features = false } serde = { version = "1.0.197", default-features = false } -serde_json = "1.0.121" +serde_json = { version = "1.0.121", default-features = false } smallvec = "1.11.0" # TODO: update to release @@ -84,6 +85,7 @@ sp-core = { git = "https://github.com/paritytech/polkadot-sdk", default-features sp-genesis-builder = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } sp-inherents = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } sp-io = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } +sp-keyring = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } sp-keystore = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } sp-offchain = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, tag = "polkadot-stable2503" } diff --git a/generic-template/runtime/Cargo.toml b/generic-template/runtime/Cargo.toml index 6b9be5e..5b14738 100644 --- a/generic-template/runtime/Cargo.toml +++ b/generic-template/runtime/Cargo.toml @@ -11,10 +11,12 @@ version = "3.0.0" targets = [ "x86_64-unknown-linux-gnu" ] [dependencies] +docify = { workspace = true } hex-literal = { workspace = true, optional = true } log = { workspace = true } parity-scale-codec = { workspace = true, features = [ "derive" ] } scale-info = { workspace = true, features = [ "derive" ] } +serde_json = { workspace = true, default-features = false, features = [ "alloc" ] } smallvec = { workspace = true } openzeppelin-pallet-abstractions = { workspace = true } @@ -54,6 +56,7 @@ sp-consensus-aura = { workspace = true } sp-core = { workspace = true } sp-genesis-builder = { workspace = true } sp-inherents = { workspace = true } +sp-keyring = { workspace = true } sp-offchain = { workspace = true } sp-runtime = { workspace = true } sp-session = { workspace = true } @@ -178,6 +181,7 @@ std = [ "polkadot-parachain-primitives/std", "polkadot-runtime-common/std", "scale-info/std", + "serde_json/std", "sp-api/std", "sp-arithmetic/std", "sp-block-builder/std", @@ -185,6 +189,7 @@ std = [ "sp-core/std", "sp-genesis-builder/std", "sp-inherents/std", + "sp-keyring/std", "sp-offchain/std", "sp-runtime/std", "sp-session/std", diff --git a/generic-template/runtime/src/genesis_config_presets.rs b/generic-template/runtime/src/genesis_config_presets.rs new file mode 100644 index 0000000..3a6c8e4 --- /dev/null +++ b/generic-template/runtime/src/genesis_config_presets.rs @@ -0,0 +1,232 @@ +use alloc::{vec, vec::Vec}; + +use cumulus_primitives_core::ParaId; +use frame_support::build_struct_json_patch; +use parachains_common::AuraId; +use serde_json::Value; +use sp_genesis_builder::PresetId; +use sp_keyring::Sr25519Keyring; + +use crate::{ + constants::currency::EXISTENTIAL_DEPOSIT, AccountId, BalancesConfig, CollatorSelectionConfig, + ParachainInfoConfig, PolkadotXcmConfig, RuntimeGenesisConfig, SessionConfig, SessionKeys, + SudoConfig, +}; + +/// The default XCM version to set in genesis config. +const SAFE_XCM_VERSION: u32 = xcm::prelude::XCM_VERSION; +/// Parachain id used for genesis config presets of parachain template. +#[docify::export_content] +pub const PARACHAIN_ID: u32 = 1000; + +/// Generate the session keys from individual elements. +/// +/// The input must be a tuple of individual keys (a single arg for now since we have just one key). +pub fn template_session_keys(keys: AuraId) -> SessionKeys { + SessionKeys { aura: keys } +} + +fn testnet_genesis( + invulnerables: Vec<(AccountId, AuraId)>, + endowed_accounts: Vec, + root: AccountId, + id: ParaId, +) -> Value { + build_struct_json_patch!(RuntimeGenesisConfig { + balances: BalancesConfig { + balances: endowed_accounts + .iter() + .cloned() + .map(|k| (k, 1u128 << 60)) + .collect::>(), + }, + parachain_info: ParachainInfoConfig { parachain_id: id }, + collator_selection: CollatorSelectionConfig { + invulnerables: invulnerables.iter().cloned().map(|(acc, _)| acc).collect::>(), + candidacy_bond: EXISTENTIAL_DEPOSIT * 16, + }, + session: SessionConfig { + keys: invulnerables + .into_iter() + .map(|(acc, aura)| { + ( + acc.clone(), // account id + acc, // validator id + template_session_keys(aura), // session keys + ) + }) + .collect::>(), + }, + polkadot_xcm: PolkadotXcmConfig { safe_xcm_version: Some(SAFE_XCM_VERSION) }, + sudo: SudoConfig { key: Some(root) }, + }) +} + +fn local_testnet_genesis() -> Value { + testnet_genesis( + // initial collators. + vec![ + (Sr25519Keyring::Alice.to_account_id(), Sr25519Keyring::Alice.public().into()), + (Sr25519Keyring::Bob.to_account_id(), Sr25519Keyring::Bob.public().into()), + ], + Sr25519Keyring::well_known().map(|k| k.to_account_id()).collect(), + Sr25519Keyring::Alice.to_account_id(), + PARACHAIN_ID.into(), + ) +} + +fn development_config_genesis() -> Value { + testnet_genesis( + // initial collators. + vec![ + (Sr25519Keyring::Alice.to_account_id(), Sr25519Keyring::Alice.public().into()), + (Sr25519Keyring::Bob.to_account_id(), Sr25519Keyring::Bob.public().into()), + ], + Sr25519Keyring::well_known().map(|k| k.to_account_id()).collect(), + Sr25519Keyring::Alice.to_account_id(), + PARACHAIN_ID.into(), + ) +} + +/// Provides the JSON representation of predefined genesis config for given `id`. +pub fn get_preset(id: &PresetId) -> Option> { + let patch = match id.as_ref() { + sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET => local_testnet_genesis(), + sp_genesis_builder::DEV_RUNTIME_PRESET => development_config_genesis(), + _ => return None, + }; + Some( + serde_json::to_string(&patch) + .expect("serialization to json is expected to work. qed.") + .into_bytes(), + ) +} + +/// List of supported presets. +pub fn preset_names() -> Vec { + vec![ + PresetId::from(sp_genesis_builder::DEV_RUNTIME_PRESET), + PresetId::from(sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET), + ] +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + /// Parse a preset bytes blob with serde_json. + fn parse_bytes(b: Vec) -> Value { + serde_json::from_slice::(&b).expect("preset must be valid JSON") + } + + /// Extract a JSON value for an AccountId by serializing the runtime AccountId directly. + /// This avoids guessing about how AccountId32 is encoded (hex string with 0x…). + fn account_json(acc: &AccountId) -> Value { + serde_json::to_value(acc).expect("AccountId must serialize to JSON") + } + + #[test] + fn local_testnet_genesis_shape_is_reasonable() { + let v = super::local_testnet_genesis(); + + // Top-level keys we care about exist + assert!(v.get("balances").is_some(), "balances missing"); + assert!(v.get("parachainInfo").is_some(), "parachainInfo missing"); + assert!(v.get("collatorSelection").is_some(), "collatorSelection missing"); + assert!(v.get("session").is_some(), "session missing"); + assert!(v.get("polkadotXcm").is_some(), "polkadotXcm missing"); + assert!(v.get("sudo").is_some(), "sudo missing"); + + // parachainId + assert_eq!(v["parachainInfo"]["parachainId"], json!(PARACHAIN_ID), "wrong parachain id"); + + // XCM version + assert_eq!( + v["polkadotXcm"]["safeXcmVersion"], + json!(Some(SAFE_XCM_VERSION)), + "wrong SAFE_XCM_VERSION" + ); + + // Endowed accounts length should match well_known() + let expected_endowed: Vec = + Sr25519Keyring::well_known().map(|k| k.to_account_id()).collect(); + let balances = + v["balances"]["balances"].as_array().expect("balances.balances must be an array"); + assert_eq!(balances.len(), expected_endowed.len(), "endowed accounts length mismatch"); + + // Sudo key must be Alice + let alice = Sr25519Keyring::Alice.to_account_id(); + assert_eq!(v["sudo"]["key"], json!(Some(account_json(&alice))), "sudo key is not Alice"); + + // Collators (invulnerables) must be Alice and Bob + let invuls = v["collatorSelection"]["invulnerables"] + .as_array() + .expect("collatorSelection.invulnerables must be an array"); + assert_eq!(invuls.len(), 2, "expected two invulnerables"); + let expected_alice = account_json(&Sr25519Keyring::Alice.to_account_id()); + let expected_bob = account_json(&Sr25519Keyring::Bob.to_account_id()); + assert_eq!(invuls[0], expected_alice, "first invulnerable must be Alice"); + assert_eq!(invuls[1], expected_bob, "second invulnerable must be Bob"); + + // candidacyBond must be EXISTENTIAL_DEPOSIT * 16 + assert_eq!( + v["collatorSelection"]["candidacyBond"], + json!(EXISTENTIAL_DEPOSIT * 16), + "wrong candidacy bond" + ); + + // Session keys: one entry per invulnerable; controller == validator + let sess = v["session"]["keys"].as_array().expect("session.keys must be an array"); + assert_eq!(sess.len(), invuls.len(), "session keys length must equal invulnerables length"); + for entry in sess { + let arr = entry + .as_array() + .expect("each session entry must be a 3-tuple [acc, validator, keys]"); + assert_eq!(arr.len(), 3, "session entry must have 3 elements"); + assert_eq!(arr[0], arr[1], "controller and validator account must match"); + // We don't assert on the actual Aura key bytes here—just presence/shape. + assert!(arr[2].get("aura").is_some(), "session keys must contain aura"); + } + } + + #[test] + fn development_genesis_mirrors_local_testnet() { + // In this template, development == local_testnet (per implementation). + let dev = super::development_config_genesis(); + let local = super::local_testnet_genesis(); + assert_eq!(dev, local, "development config should mirror local_testnet"); + } + + #[test] + fn get_preset_roundtrips_known_ids() { + // LOCAL_TESTNET + let p = + super::get_preset(&PresetId::from(sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET)) + .expect("local preset should exist"); + let from_api = parse_bytes(p); + let from_fn = super::local_testnet_genesis(); + assert_eq!(from_api, from_fn, "local_testnet preset must match function output"); + + // DEV + let p = super::get_preset(&PresetId::from(sp_genesis_builder::DEV_RUNTIME_PRESET)) + .expect("dev preset should exist"); + let from_api = parse_bytes(p); + let from_fn = super::development_config_genesis(); + assert_eq!(from_api, from_fn, "dev preset must match function output"); + } + + #[test] + fn preset_names_lists_supported() { + let names = super::preset_names(); + assert!( + names.contains(&PresetId::from(sp_genesis_builder::DEV_RUNTIME_PRESET)), + "DEV preset should be listed" + ); + assert!( + names.contains(&PresetId::from(sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET)), + "LOCAL_TESTNET preset should be listed" + ); + } +} diff --git a/generic-template/runtime/src/lib.rs b/generic-template/runtime/src/lib.rs index 718a49e..cad14eb 100644 --- a/generic-template/runtime/src/lib.rs +++ b/generic-template/runtime/src/lib.rs @@ -10,12 +10,14 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); pub mod apis; pub mod configs; pub mod constants; +mod genesis_config_presets; mod types; mod weights; use frame_support::weights::{ WeightToFeeCoefficient, WeightToFeeCoefficients, WeightToFeePolynomial, }; +pub use genesis_config_presets::PARACHAIN_ID; use smallvec::smallvec; pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_runtime::impl_opaque_keys;