Add state-root command and network support to lcli (#4492)
## Proposed Changes * Add `lcli state-root` command for computing the hash tree root of a `BeaconState`. * Add a `--network` flag which can be used instead of `--testnet-dir` to set the network, e.g. Mainnet, Goerli, Gnosis. * Use the new network flag in `transition-blocks`, `skip-slots`, and `block-root`, which previously only supported mainnet. * **BREAKING CHANGE** Remove the default value of `~/.lighthouse/testnet` from `--testnet-dir`. This may have made sense in previous versions where `lcli` was more testnet focussed, but IMO it is an unnecessary complication and foot-gun today.
This commit is contained in:
parent
c25825a539
commit
420e9c490e
@ -31,14 +31,19 @@ use clap::ArgMatches;
|
|||||||
use clap_utils::{parse_optional, parse_required};
|
use clap_utils::{parse_optional, parse_required};
|
||||||
use environment::Environment;
|
use environment::Environment;
|
||||||
use eth2::{types::BlockId, BeaconNodeHttpClient, SensitiveUrl, Timeouts};
|
use eth2::{types::BlockId, BeaconNodeHttpClient, SensitiveUrl, Timeouts};
|
||||||
|
use eth2_network_config::Eth2NetworkConfig;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use types::{EthSpec, FullPayload, SignedBeaconBlock};
|
use types::{EthSpec, FullPayload, SignedBeaconBlock};
|
||||||
|
|
||||||
const HTTP_TIMEOUT: Duration = Duration::from_secs(5);
|
const HTTP_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
pub fn run<T: EthSpec>(env: Environment<T>, matches: &ArgMatches) -> Result<(), String> {
|
pub fn run<T: EthSpec>(
|
||||||
let spec = &T::default_spec();
|
env: Environment<T>,
|
||||||
|
network_config: Eth2NetworkConfig,
|
||||||
|
matches: &ArgMatches,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let spec = &network_config.chain_spec::<T>()?;
|
||||||
let executor = env.core_context().executor;
|
let executor = env.core_context().executor;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
136
lcli/src/main.rs
136
lcli/src/main.rs
@ -15,11 +15,13 @@ mod new_testnet;
|
|||||||
mod parse_ssz;
|
mod parse_ssz;
|
||||||
mod replace_state_pubkeys;
|
mod replace_state_pubkeys;
|
||||||
mod skip_slots;
|
mod skip_slots;
|
||||||
|
mod state_root;
|
||||||
mod transition_blocks;
|
mod transition_blocks;
|
||||||
|
|
||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||||
use clap_utils::parse_path_with_default_in_home_dir;
|
use clap_utils::parse_optional;
|
||||||
use environment::{EnvironmentBuilder, LoggerConfig};
|
use environment::{EnvironmentBuilder, LoggerConfig};
|
||||||
|
use eth2_network_config::Eth2NetworkConfig;
|
||||||
use parse_ssz::run_parse_ssz;
|
use parse_ssz::run_parse_ssz;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process;
|
use std::process;
|
||||||
@ -50,7 +52,16 @@ fn main() {
|
|||||||
.value_name("PATH")
|
.value_name("PATH")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.global(true)
|
.global(true)
|
||||||
.help("The testnet dir. Defaults to ~/.lighthouse/testnet"),
|
.help("The testnet dir."),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("network")
|
||||||
|
.long("network")
|
||||||
|
.value_name("NAME")
|
||||||
|
.takes_value(true)
|
||||||
|
.global(true)
|
||||||
|
.help("The network to use. Defaults to mainnet.")
|
||||||
|
.conflicts_with("testnet-dir")
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("skip-slots")
|
SubCommand::with_name("skip-slots")
|
||||||
@ -126,7 +137,7 @@ fn main() {
|
|||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.conflicts_with("beacon-url")
|
.conflicts_with("beacon-url")
|
||||||
.requires("block-path")
|
.requires("block-path")
|
||||||
.help("Path to load a BeaconState from file as SSZ."),
|
.help("Path to load a BeaconState from as SSZ."),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("block-path")
|
Arg::with_name("block-path")
|
||||||
@ -135,7 +146,7 @@ fn main() {
|
|||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.conflicts_with("beacon-url")
|
.conflicts_with("beacon-url")
|
||||||
.requires("pre-state-path")
|
.requires("pre-state-path")
|
||||||
.help("Path to load a SignedBeaconBlock from file as SSZ."),
|
.help("Path to load a SignedBeaconBlock from as SSZ."),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("post-state-output-path")
|
Arg::with_name("post-state-output-path")
|
||||||
@ -808,14 +819,14 @@ fn main() {
|
|||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("block-root")
|
SubCommand::with_name("block-root")
|
||||||
.about("Computes the block root of some block")
|
.about("Computes the block root of some block.")
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("block-path")
|
Arg::with_name("block-path")
|
||||||
.long("block-path")
|
.long("block-path")
|
||||||
.value_name("PATH")
|
.value_name("PATH")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.conflicts_with("beacon-url")
|
.conflicts_with("beacon-url")
|
||||||
.help("Path to load a SignedBeaconBlock from file as SSZ."),
|
.help("Path to load a SignedBeaconBlock from as SSZ."),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("beacon-url")
|
Arg::with_name("beacon-url")
|
||||||
@ -841,6 +852,41 @@ fn main() {
|
|||||||
.help("Number of repeat runs, useful for benchmarking."),
|
.help("Number of repeat runs, useful for benchmarking."),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("state-root")
|
||||||
|
.about("Computes the state root of some state.")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("state-path")
|
||||||
|
.long("state-path")
|
||||||
|
.value_name("PATH")
|
||||||
|
.takes_value(true)
|
||||||
|
.conflicts_with("beacon-url")
|
||||||
|
.help("Path to load a BeaconState from as SSZ."),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("beacon-url")
|
||||||
|
.long("beacon-url")
|
||||||
|
.value_name("URL")
|
||||||
|
.takes_value(true)
|
||||||
|
.help("URL to a beacon-API provider."),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("state-id")
|
||||||
|
.long("state-id")
|
||||||
|
.value_name("BLOCK_ID")
|
||||||
|
.takes_value(true)
|
||||||
|
.requires("beacon-url")
|
||||||
|
.help("Identifier for a state as per beacon-API standards (slot, root, etc.)"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("runs")
|
||||||
|
.long("runs")
|
||||||
|
.value_name("INTEGER")
|
||||||
|
.takes_value(true)
|
||||||
|
.default_value("1")
|
||||||
|
.help("Number of repeat runs, useful for benchmarking."),
|
||||||
|
)
|
||||||
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
let result = matches
|
let result = matches
|
||||||
@ -887,17 +933,44 @@ fn run<T: EthSpec>(
|
|||||||
.build()
|
.build()
|
||||||
.map_err(|e| format!("should build env: {:?}", e))?;
|
.map_err(|e| format!("should build env: {:?}", e))?;
|
||||||
|
|
||||||
let testnet_dir = parse_path_with_default_in_home_dir(
|
// Determine testnet-dir path or network name depending on CLI flags.
|
||||||
matches,
|
let (testnet_dir, network_name) =
|
||||||
"testnet-dir",
|
if let Some(testnet_dir) = parse_optional::<PathBuf>(matches, "testnet-dir")? {
|
||||||
PathBuf::from(directory::DEFAULT_ROOT_DIR).join("testnet"),
|
(Some(testnet_dir), None)
|
||||||
)?;
|
} else {
|
||||||
|
let network_name =
|
||||||
|
parse_optional(matches, "network")?.unwrap_or_else(|| "mainnet".to_string());
|
||||||
|
(None, Some(network_name))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lazily load either the testnet dir or the network config, as required.
|
||||||
|
// Some subcommands like new-testnet need the testnet dir but not the network config.
|
||||||
|
let get_testnet_dir = || testnet_dir.clone().ok_or("testnet-dir is required");
|
||||||
|
let get_network_config = || {
|
||||||
|
if let Some(testnet_dir) = &testnet_dir {
|
||||||
|
Eth2NetworkConfig::load(testnet_dir.clone()).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"Unable to open testnet dir at {}: {}",
|
||||||
|
testnet_dir.display(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let network_name = network_name.ok_or("no network name or testnet-dir provided")?;
|
||||||
|
Eth2NetworkConfig::constant(&network_name)?.ok_or("invalid network name".into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match matches.subcommand() {
|
match matches.subcommand() {
|
||||||
("transition-blocks", Some(matches)) => transition_blocks::run::<T>(env, matches)
|
("transition-blocks", Some(matches)) => {
|
||||||
.map_err(|e| format!("Failed to transition blocks: {}", e)),
|
let network_config = get_network_config()?;
|
||||||
|
transition_blocks::run::<T>(env, network_config, matches)
|
||||||
|
.map_err(|e| format!("Failed to transition blocks: {}", e))
|
||||||
|
}
|
||||||
("skip-slots", Some(matches)) => {
|
("skip-slots", Some(matches)) => {
|
||||||
skip_slots::run::<T>(env, matches).map_err(|e| format!("Failed to skip slots: {}", e))
|
let network_config = get_network_config()?;
|
||||||
|
skip_slots::run::<T>(env, network_config, matches)
|
||||||
|
.map_err(|e| format!("Failed to skip slots: {}", e))
|
||||||
}
|
}
|
||||||
("pretty-ssz", Some(matches)) => {
|
("pretty-ssz", Some(matches)) => {
|
||||||
run_parse_ssz::<T>(matches).map_err(|e| format!("Failed to pretty print hex: {}", e))
|
run_parse_ssz::<T>(matches).map_err(|e| format!("Failed to pretty print hex: {}", e))
|
||||||
@ -906,22 +979,33 @@ fn run<T: EthSpec>(
|
|||||||
deploy_deposit_contract::run::<T>(env, matches)
|
deploy_deposit_contract::run::<T>(env, matches)
|
||||||
.map_err(|e| format!("Failed to run deploy-deposit-contract command: {}", e))
|
.map_err(|e| format!("Failed to run deploy-deposit-contract command: {}", e))
|
||||||
}
|
}
|
||||||
("eth1-genesis", Some(matches)) => eth1_genesis::run::<T>(env, testnet_dir, matches)
|
("eth1-genesis", Some(matches)) => {
|
||||||
.map_err(|e| format!("Failed to run eth1-genesis command: {}", e)),
|
let testnet_dir = get_testnet_dir()?;
|
||||||
("interop-genesis", Some(matches)) => interop_genesis::run::<T>(testnet_dir, matches)
|
eth1_genesis::run::<T>(env, testnet_dir, matches)
|
||||||
.map_err(|e| format!("Failed to run interop-genesis command: {}", e)),
|
.map_err(|e| format!("Failed to run eth1-genesis command: {}", e))
|
||||||
|
}
|
||||||
|
("interop-genesis", Some(matches)) => {
|
||||||
|
let testnet_dir = get_testnet_dir()?;
|
||||||
|
interop_genesis::run::<T>(testnet_dir, matches)
|
||||||
|
.map_err(|e| format!("Failed to run interop-genesis command: {}", e))
|
||||||
|
}
|
||||||
("change-genesis-time", Some(matches)) => {
|
("change-genesis-time", Some(matches)) => {
|
||||||
|
let testnet_dir = get_testnet_dir()?;
|
||||||
change_genesis_time::run::<T>(testnet_dir, matches)
|
change_genesis_time::run::<T>(testnet_dir, matches)
|
||||||
.map_err(|e| format!("Failed to run change-genesis-time command: {}", e))
|
.map_err(|e| format!("Failed to run change-genesis-time command: {}", e))
|
||||||
}
|
}
|
||||||
("create-payload-header", Some(matches)) => create_payload_header::run::<T>(matches)
|
("create-payload-header", Some(matches)) => create_payload_header::run::<T>(matches)
|
||||||
.map_err(|e| format!("Failed to run create-payload-header command: {}", e)),
|
.map_err(|e| format!("Failed to run create-payload-header command: {}", e)),
|
||||||
("replace-state-pubkeys", Some(matches)) => {
|
("replace-state-pubkeys", Some(matches)) => {
|
||||||
|
let testnet_dir = get_testnet_dir()?;
|
||||||
replace_state_pubkeys::run::<T>(testnet_dir, matches)
|
replace_state_pubkeys::run::<T>(testnet_dir, matches)
|
||||||
.map_err(|e| format!("Failed to run replace-state-pubkeys command: {}", e))
|
.map_err(|e| format!("Failed to run replace-state-pubkeys command: {}", e))
|
||||||
}
|
}
|
||||||
("new-testnet", Some(matches)) => new_testnet::run::<T>(testnet_dir, matches)
|
("new-testnet", Some(matches)) => {
|
||||||
.map_err(|e| format!("Failed to run new_testnet command: {}", e)),
|
let testnet_dir = get_testnet_dir()?;
|
||||||
|
new_testnet::run::<T>(testnet_dir, matches)
|
||||||
|
.map_err(|e| format!("Failed to run new_testnet command: {}", e))
|
||||||
|
}
|
||||||
("check-deposit-data", Some(matches)) => check_deposit_data::run(matches)
|
("check-deposit-data", Some(matches)) => check_deposit_data::run(matches)
|
||||||
.map_err(|e| format!("Failed to run check-deposit-data command: {}", e)),
|
.map_err(|e| format!("Failed to run check-deposit-data command: {}", e)),
|
||||||
("generate-bootnode-enr", Some(matches)) => generate_bootnode_enr::run::<T>(matches)
|
("generate-bootnode-enr", Some(matches)) => generate_bootnode_enr::run::<T>(matches)
|
||||||
@ -932,8 +1016,16 @@ fn run<T: EthSpec>(
|
|||||||
.map_err(|e| format!("Failed to run mnemonic-validators command: {}", e)),
|
.map_err(|e| format!("Failed to run mnemonic-validators command: {}", e)),
|
||||||
("indexed-attestations", Some(matches)) => indexed_attestations::run::<T>(matches)
|
("indexed-attestations", Some(matches)) => indexed_attestations::run::<T>(matches)
|
||||||
.map_err(|e| format!("Failed to run indexed-attestations command: {}", e)),
|
.map_err(|e| format!("Failed to run indexed-attestations command: {}", e)),
|
||||||
("block-root", Some(matches)) => block_root::run::<T>(env, matches)
|
("block-root", Some(matches)) => {
|
||||||
.map_err(|e| format!("Failed to run block-root command: {}", e)),
|
let network_config = get_network_config()?;
|
||||||
|
block_root::run::<T>(env, network_config, matches)
|
||||||
|
.map_err(|e| format!("Failed to run block-root command: {}", e))
|
||||||
|
}
|
||||||
|
("state-root", Some(matches)) => {
|
||||||
|
let network_config = get_network_config()?;
|
||||||
|
state_root::run::<T>(env, network_config, matches)
|
||||||
|
.map_err(|e| format!("Failed to run state-root command: {}", e))
|
||||||
|
}
|
||||||
(other, _) => Err(format!("Unknown subcommand {}. See --help.", other)),
|
(other, _) => Err(format!("Unknown subcommand {}. See --help.", other)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ use clap::ArgMatches;
|
|||||||
use clap_utils::{parse_optional, parse_required};
|
use clap_utils::{parse_optional, parse_required};
|
||||||
use environment::Environment;
|
use environment::Environment;
|
||||||
use eth2::{types::StateId, BeaconNodeHttpClient, SensitiveUrl, Timeouts};
|
use eth2::{types::StateId, BeaconNodeHttpClient, SensitiveUrl, Timeouts};
|
||||||
|
use eth2_network_config::Eth2NetworkConfig;
|
||||||
use ssz::Encode;
|
use ssz::Encode;
|
||||||
use state_processing::state_advance::{complete_state_advance, partial_state_advance};
|
use state_processing::state_advance::{complete_state_advance, partial_state_advance};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
@ -59,8 +60,12 @@ use types::{BeaconState, CloneConfig, EthSpec, Hash256};
|
|||||||
|
|
||||||
const HTTP_TIMEOUT: Duration = Duration::from_secs(10);
|
const HTTP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
pub fn run<T: EthSpec>(env: Environment<T>, matches: &ArgMatches) -> Result<(), String> {
|
pub fn run<T: EthSpec>(
|
||||||
let spec = &T::default_spec();
|
env: Environment<T>,
|
||||||
|
network_config: Eth2NetworkConfig,
|
||||||
|
matches: &ArgMatches,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let spec = &network_config.chain_spec::<T>()?;
|
||||||
let executor = env.core_context().executor;
|
let executor = env.core_context().executor;
|
||||||
|
|
||||||
let output_path: Option<PathBuf> = parse_optional(matches, "output-path")?;
|
let output_path: Option<PathBuf> = parse_optional(matches, "output-path")?;
|
||||||
|
76
lcli/src/state_root.rs
Normal file
76
lcli/src/state_root.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use crate::transition_blocks::load_from_ssz_with;
|
||||||
|
use clap::ArgMatches;
|
||||||
|
use clap_utils::{parse_optional, parse_required};
|
||||||
|
use environment::Environment;
|
||||||
|
use eth2::{types::StateId, BeaconNodeHttpClient, SensitiveUrl, Timeouts};
|
||||||
|
use eth2_network_config::Eth2NetworkConfig;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use types::{BeaconState, EthSpec};
|
||||||
|
|
||||||
|
const HTTP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
pub fn run<T: EthSpec>(
|
||||||
|
env: Environment<T>,
|
||||||
|
network_config: Eth2NetworkConfig,
|
||||||
|
matches: &ArgMatches,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let executor = env.core_context().executor;
|
||||||
|
|
||||||
|
let spec = &network_config.chain_spec::<T>()?;
|
||||||
|
|
||||||
|
let state_path: Option<PathBuf> = parse_optional(matches, "state-path")?;
|
||||||
|
let beacon_url: Option<SensitiveUrl> = parse_optional(matches, "beacon-url")?;
|
||||||
|
let runs: usize = parse_required(matches, "runs")?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Using {} network ({} spec)",
|
||||||
|
spec.config_name.as_deref().unwrap_or("unknown"),
|
||||||
|
T::spec_name()
|
||||||
|
);
|
||||||
|
info!("Doing {} runs", runs);
|
||||||
|
|
||||||
|
let state = match (state_path, beacon_url) {
|
||||||
|
(Some(state_path), None) => {
|
||||||
|
info!("State path: {:?}", state_path);
|
||||||
|
load_from_ssz_with(&state_path, spec, BeaconState::from_ssz_bytes)?
|
||||||
|
}
|
||||||
|
(None, Some(beacon_url)) => {
|
||||||
|
let state_id: StateId = parse_required(matches, "state-id")?;
|
||||||
|
let client = BeaconNodeHttpClient::new(beacon_url, Timeouts::set_all(HTTP_TIMEOUT));
|
||||||
|
executor
|
||||||
|
.handle()
|
||||||
|
.ok_or("shutdown in progress")?
|
||||||
|
.block_on(async move {
|
||||||
|
client
|
||||||
|
.get_debug_beacon_states::<T>(state_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to download state: {:?}", e))
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("Failed to complete task: {:?}", e))?
|
||||||
|
.ok_or_else(|| format!("Unable to locate state at {:?}", state_id))?
|
||||||
|
.data
|
||||||
|
}
|
||||||
|
_ => return Err("must supply either --state-path or --beacon-url".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Perform the core "runs".
|
||||||
|
*/
|
||||||
|
let mut state_root = None;
|
||||||
|
for i in 0..runs {
|
||||||
|
let mut state = state.clone();
|
||||||
|
let timer = Instant::now();
|
||||||
|
state_root = Some(
|
||||||
|
state
|
||||||
|
.update_tree_hash_cache()
|
||||||
|
.map_err(|e| format!("error computing state root: {e:?}"))?,
|
||||||
|
);
|
||||||
|
info!("Run {}: {:?}", i, timer.elapsed());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(state_root) = state_root {
|
||||||
|
info!("State root is {:?}", state_root);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -71,6 +71,7 @@ use eth2::{
|
|||||||
types::{BlockId, StateId},
|
types::{BlockId, StateId},
|
||||||
BeaconNodeHttpClient, SensitiveUrl, Timeouts,
|
BeaconNodeHttpClient, SensitiveUrl, Timeouts,
|
||||||
};
|
};
|
||||||
|
use eth2_network_config::Eth2NetworkConfig;
|
||||||
use ssz::Encode;
|
use ssz::Encode;
|
||||||
use state_processing::{
|
use state_processing::{
|
||||||
block_signature_verifier::BlockSignatureVerifier, per_block_processing, per_slot_processing,
|
block_signature_verifier::BlockSignatureVerifier, per_block_processing, per_slot_processing,
|
||||||
@ -94,8 +95,12 @@ struct Config {
|
|||||||
exclude_post_block_thc: bool,
|
exclude_post_block_thc: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run<T: EthSpec>(env: Environment<T>, matches: &ArgMatches) -> Result<(), String> {
|
pub fn run<T: EthSpec>(
|
||||||
let spec = &T::default_spec();
|
env: Environment<T>,
|
||||||
|
network_config: Eth2NetworkConfig,
|
||||||
|
matches: &ArgMatches,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let spec = &network_config.chain_spec::<T>()?;
|
||||||
let executor = env.core_context().executor;
|
let executor = env.core_context().executor;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
Loading…
Reference in New Issue
Block a user