From 6c0e9fb10700a836bd2ab7eeb2d7a1053e41927a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 15 Oct 2019 15:07:56 +0100 Subject: [PATCH] chain spec builder: add generate mode for authority keys (#3811) * core: use trait object for genesis constructor * chain-spec-builder: use structopt * chain-spec-builder: add new command to generate authority keys * chain-spec-builder: use ? in main * chain-spec-builder: fix stored and printed suri from seed * chain-spec-builder: add comment about created keystore folders * chain-spec-builder: simplify file write --- substrate/Cargo.lock | 6 +- substrate/core/chain-spec/src/chain_spec.rs | 11 +- .../test-utils/chain-spec-builder/Cargo.toml | 6 +- .../test-utils/chain-spec-builder/src/cli.yml | 24 -- .../test-utils/chain-spec-builder/src/main.rs | 260 ++++++++++++++++-- 5 files changed, 250 insertions(+), 57 deletions(-) delete mode 100644 substrate/test-utils/chain-spec-builder/src/cli.yml diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index 5a17eaed31..b8dade5e24 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -400,10 +400,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "chain-spec-builder" version = "2.0.0" dependencies = [ - "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", "node-cli 2.0.0", + "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "structopt 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "substrate-keystore 2.0.0", "substrate-primitives 2.0.0", - "substrate-service 2.0.0", ] [[package]] diff --git a/substrate/core/chain-spec/src/chain_spec.rs b/substrate/core/chain-spec/src/chain_spec.rs index b4c57f1e03..8d72effc7b 100644 --- a/substrate/core/chain-spec/src/chain_spec.rs +++ b/substrate/core/chain-spec/src/chain_spec.rs @@ -20,6 +20,7 @@ use std::borrow::Cow; use std::collections::HashMap; use std::fs::File; use std::path::PathBuf; +use std::rc::Rc; use serde::{Serialize, Deserialize}; use primitives::storage::{StorageKey, StorageData}; use sr_primitives::{BuildStorage, StorageOverlay, ChildrenStorageOverlay}; @@ -31,7 +32,7 @@ use tel::TelemetryEndpoints; enum GenesisSource { File(PathBuf), Binary(Cow<'static, [u8]>), - Factory(fn() -> G), + Factory(Rc G>), } impl Clone for GenesisSource { @@ -39,7 +40,7 @@ impl Clone for GenesisSource { match *self { GenesisSource::File(ref path) => GenesisSource::File(path.clone()), GenesisSource::Binary(ref d) => GenesisSource::Binary(d.clone()), - GenesisSource::Factory(f) => GenesisSource::Factory(f), + GenesisSource::Factory(ref f) => GenesisSource::Factory(f.clone()), } } } @@ -187,10 +188,10 @@ impl ChainSpec { } /// Create hardcoded spec. - pub fn from_genesis( + pub fn from_genesis G + 'static>( name: &str, id: &str, - constructor: fn() -> G, + constructor: F, boot_nodes: Vec, telemetry_endpoints: Option, protocol_id: Option<&str>, @@ -211,7 +212,7 @@ impl ChainSpec { ChainSpec { spec, - genesis: GenesisSource::Factory(constructor), + genesis: GenesisSource::Factory(Rc::new(constructor)), } } } diff --git a/substrate/test-utils/chain-spec-builder/Cargo.toml b/substrate/test-utils/chain-spec-builder/Cargo.toml index 478e230505..324d33cbf0 100644 --- a/substrate/test-utils/chain-spec-builder/Cargo.toml +++ b/substrate/test-utils/chain-spec-builder/Cargo.toml @@ -5,7 +5,9 @@ authors = ["Parity Technologies "] edition = "2018" [dependencies] -clap = { version = "2.33.0", features = ["yaml"] } +ansi_term = "0.12.1" +keystore = { package = "substrate-keystore", path = "../../core/keystore" } node-cli = { path = "../../node/cli" } primitives = { package = "substrate-primitives", path = "../../core/primitives" } -substrate-service = { path = "../../core/service" } +rand = "0.7.2" +structopt = "0.3.3" diff --git a/substrate/test-utils/chain-spec-builder/src/cli.yml b/substrate/test-utils/chain-spec-builder/src/cli.yml deleted file mode 100644 index f4b1cb7e2e..0000000000 --- a/substrate/test-utils/chain-spec-builder/src/cli.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: chain-spec-builder -author: "azban " -about: Utility for creating chain specs primarily for testing -args: -- initial_authority_seed: - short: a - value_name: INITIAL_AUTHORITY_SEED - help: Initial authority seed - takes_value: true - multiple: true - required: true -- endowed_account_seed: - short: e - value_name: ENDOWED_ACCOUNT_SEED - help: Endowed account seed - takes_value: true - multiple: true - required: true -- sudo_key_seed: - short: u - value_name: SUDO_KEY_SEED - help: Sudo key seed - takes_value: true - required: true diff --git a/substrate/test-utils/chain-spec-builder/src/main.rs b/substrate/test-utils/chain-spec-builder/src/main.rs index 8f59c73493..22c5253b06 100644 --- a/substrate/test-utils/chain-spec-builder/src/main.rs +++ b/substrate/test-utils/chain-spec-builder/src/main.rs @@ -1,49 +1,261 @@ -use clap::{App, load_yaml}; +// Copyright 2019 Parity Technologies (UK) Ltd. +// This file is part of Substrate. +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +use std::{fs, path::{Path, PathBuf}}; + +use ansi_term::Style; +use rand::{Rng, distributions::Alphanumeric, rngs::OsRng}; +use structopt::StructOpt; + +use keystore::{Store as Keystore}; use node_cli::chain_spec::{self, AccountId}; -use substrate_service::chain_ops::build_spec; +use primitives::{crypto::{Public, Ss58Codec}, traits::BareCryptoStore}; -fn genesis_constructor() -> chain_spec::GenesisConfig { - let yaml = load_yaml!("./cli.yml"); - let matches = App::from_yaml(yaml).get_matches(); - let authorities = matches.values_of("initial_authority_seed") - .unwrap() +/// A utility to easily create a testnet chain spec definition with a given set +/// of authorities and endowed accounts and/or generate random accounts. +#[derive(StructOpt)] +#[structopt(rename_all = "kebab-case")] +enum ChainSpecBuilder { + /// Create a new chain spec with the given authorities, endowed and sudo + /// accounts. + New { + /// Authority key seed. + #[structopt(long, short, required = true)] + authority_seeds: Vec, + /// Endowed account address (SS58 format). + #[structopt(long, short)] + endowed_accounts: Vec, + /// Sudo account address (SS58 format). + #[structopt(long, short)] + sudo_account: String, + /// The path where the chain spec should be saved. + #[structopt(long, short, default_value = "./chain_spec.json")] + chain_spec_path: PathBuf, + }, + /// Create a new chain spec with the given number of authorities and endowed + /// accounts. Random keys will be generated as required. + Generate { + /// The number of authorities. + #[structopt(long, short)] + authorities: usize, + /// The number of endowed accounts. + #[structopt(long, short, default_value = "0")] + endowed: usize, + /// The path where the chain spec should be saved. + #[structopt(long, short, default_value = "./chain_spec.json")] + chain_spec_path: PathBuf, + /// Path to use when saving generated keystores for each authority. + /// + /// At this path, a new folder will be created for each authority's + /// keystore named `auth-$i` where `i` is the authority index, i.e. + /// `auth-0`, `auth-1`, etc. + #[structopt(long, short)] + keystore_path: Option, + }, +} + +impl ChainSpecBuilder { + /// Returns the path where the chain spec should be saved. + fn chain_spec_path(&self) -> &Path { + match self { + ChainSpecBuilder::New { chain_spec_path, .. } => + chain_spec_path.as_path(), + ChainSpecBuilder::Generate { chain_spec_path, .. } => + chain_spec_path.as_path(), + } + } +} + +fn genesis_constructor( + authority_seeds: &[String], + endowed_accounts: &[AccountId], + sudo_account: &AccountId, +) -> chain_spec::GenesisConfig { + let authorities = authority_seeds + .iter() + .map(AsRef::as_ref) .map(chain_spec::get_authority_keys_from_seed) - .collect(); - - let endowed_accounts = matches.values_of("endowed_account_seed") - .unwrap() - .map(chain_spec::get_from_seed::) - .collect(); - - let sudo_key_seed = matches.value_of("sudo_key_seed").unwrap(); - let sudo_key = chain_spec::get_from_seed::(sudo_key_seed); + .collect::>(); let enable_println = true; chain_spec::testnet_genesis( authorities, - sudo_key.into(), - Some(endowed_accounts), + sudo_account.clone(), + Some(endowed_accounts.to_vec()), enable_println, ) } -fn generate_chain_spec() -> String { +fn generate_chain_spec( + authority_seeds: Vec, + endowed_accounts: Vec, + sudo_account: String, +) -> Result { + let parse_account = |address: &String| { + AccountId::from_string(address) + .map_err(|err| format!("Failed to parse account address: {:?}", err)) + }; + + let endowed_accounts = endowed_accounts + .iter() + .map(parse_account) + .collect::, String>>()?; + + let sudo_account = parse_account(&sudo_account)?; + let chain_spec = chain_spec::ChainSpec::from_genesis( "Custom", "custom", - genesis_constructor, + move || genesis_constructor(&authority_seeds, &endowed_accounts, &sudo_account), vec![], None, None, None, Default::default(), ); - build_spec(chain_spec, false).unwrap() + + chain_spec.to_json(false).map_err(|err| err.to_string()) } -fn main() { - let json = generate_chain_spec(); - println!("{}", json); +fn generate_authority_keys_and_store( + seeds: &[String], + keystore_path: &Path, +) -> Result<(), String> { + for (n, seed) in seeds.into_iter().enumerate() { + let keystore = Keystore::open( + keystore_path.join(format!("auth-{}", n)), + None, + ).map_err(|err| err.to_string())?; + + let (_, _, grandpa, babe, im_online) = + chain_spec::get_authority_keys_from_seed(seed); + + let insert_key = |key_type, public| { + keystore.write().insert_unknown( + key_type, + &format!("//{}", seed), + public, + ).map_err(|_| format!("Failed to insert key: {}", grandpa)) + }; + + insert_key( + primitives::crypto::key_types::BABE, + babe.as_slice(), + )?; + + insert_key( + primitives::crypto::key_types::GRANDPA, + grandpa.as_slice(), + )?; + + insert_key( + primitives::crypto::key_types::IM_ONLINE, + im_online.as_slice(), + )?; + } + + Ok(()) +} + +fn print_seeds( + authority_seeds: &[String], + endowed_seeds: &[String], + sudo_seed: &str, +) { + let header = Style::new().bold().underline(); + let entry = Style::new().bold(); + + println!("{}", header.paint("Authority seeds")); + + for (n, seed) in authority_seeds.iter().enumerate() { + println!("{} //{}", + entry.paint(format!("auth-{}:", n)), + seed, + ); + } + + println!(); + + if !endowed_seeds.is_empty() { + println!("{}", header.paint("Endowed seeds")); + for (n, seed) in endowed_seeds.iter().enumerate() { + println!("{} //{}", + entry.paint(format!("endowed-{}:", n)), + seed, + ); + } + + println!(); + } + + println!("{}", header.paint("Sudo seed")); + println!("//{}", sudo_seed); +} + +fn main() -> Result<(), String> { + let builder = ChainSpecBuilder::from_args(); + let chain_spec_path = builder.chain_spec_path().to_path_buf(); + + let (authority_seeds, endowed_accounts, sudo_account) = match builder { + ChainSpecBuilder::Generate { authorities, endowed, keystore_path, .. } => { + let authorities = authorities.max(1); + let rand_str = || -> String { + OsRng.sample_iter(&Alphanumeric) + .take(32) + .collect() + }; + + let authority_seeds = (0..authorities).map(|_| rand_str()).collect::>(); + let endowed_seeds = (0..endowed).map(|_| rand_str()).collect::>(); + let sudo_seed = rand_str(); + + print_seeds( + &authority_seeds, + &endowed_seeds, + &sudo_seed, + ); + + if let Some(keystore_path) = keystore_path { + generate_authority_keys_and_store( + &authority_seeds, + &keystore_path, + )?; + } + + let endowed_accounts = endowed_seeds.iter().map(|seed| { + chain_spec::get_from_seed::(seed) + .to_ss58check() + }).collect(); + + let sudo_account = chain_spec::get_from_seed::(&sudo_seed) + .to_ss58check(); + + (authority_seeds, endowed_accounts, sudo_account) + }, + ChainSpecBuilder::New { authority_seeds, endowed_accounts, sudo_account, .. } => { + (authority_seeds, endowed_accounts, sudo_account) + }, + }; + + let json = generate_chain_spec( + authority_seeds, + endowed_accounts, + sudo_account, + )?; + + fs::write(chain_spec_path, json).map_err(|err| err.to_string()) }