Migration testing runtime API/Bot (#8038)

* A clean new attempt

* Checkpoint to move remote.

* A lot of dependency wiring to make it feature gated.

* bad macro, bad macro.

* Undo the DB mess.

* Update frame/support/src/traits.rs

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>

* Apply suggestions from code review

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>

* unbreak the build

* Update frame/try-runtime/src/lib.rs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Update utils/frame/try-runtime/cli/Cargo.toml

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>

* Update frame/try-runtime/Cargo.toml

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>

* Address most review grumbles.

* Fix build

* Add some comments

* Remove allowing one pallet at a time.

* More grumbles.

* relocate remote-ext

* Fix build

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>
Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
This commit is contained in:
Kian Paimani
2021-02-19 14:52:09 +00:00
committed by GitHub
parent 82e52b8be8
commit 16a27c28a9
24 changed files with 991 additions and 40 deletions
@@ -0,0 +1,34 @@
[package]
name = "remote-externalities"
version = "0.9.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
license = "Apache-2.0"
homepage = "https://substrate.dev"
repository = "https://github.com/paritytech/substrate/"
description = "An externalities provided environemnt that can load itself from remote nodes or cache files"
readme = "README.md"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
jsonrpc-core-client = { version = "15.1.0", features = ["http"] }
sc-rpc-api = { version = "0.9.0", path = "../../../client/rpc-api" }
sc-rpc = { version = "3.0.0", path = "../../../client/rpc" }
futures = "0.1.29"
hex-literal = "0.3.1"
env_logger = "0.8.2"
log = "0.4.11"
bincode = "1.3.1"
tokio = "0.1.22"
sp-io = { version = "3.0.0", path = "../../../primitives/io" }
sp-core = { version = "3.0.0", path = "../../../primitives/core" }
[dev-dependencies]
async-std = { version = "1.6.5", features = ["attributes"] }
[features]
remote-test = []
@@ -0,0 +1,454 @@
// This file is part of Substrate.
// Copyright (C) 2020-2021 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! # Remote Externalities
//!
//! An equivalent of `sp_io::TestExternalities` that can load its state from a remote substrate
//! based chain, or a local cache file.
//!
//! #### Runtime to Test Against
//!
//! While not absolutely necessary, you most likely need a `Runtime` equivalent in your test setup
//! through which you can infer storage types. There are two options here:
//!
//! 1. Build a mock runtime, similar how to you would build one in a pallet test (see example
//! below). The very important point here is that this mock needs to hold real values for types
//! that matter for you, based on the chain of interest. Some typical ones are:
//!
//! - `sp_runtime::AccountId32` as `AccountId`.
//! - `u32` as `BlockNumber`.
//! - `u128` as Balance.
//!
//! Once you have your `Runtime`, you can use it for storage type resolution and do things like
//! `<my_pallet::Pallet<Runtime>>::storage_getter()` or `<my_pallet::StorageItem<Runtime>>::get()`.
//!
//! 2. Or, you can use a real runtime.
//!
//! ### Example
//!
//! With a test runtime
//!
//! ```ignore
//! use remote_externalities::Builder;
//!
//! #[derive(Clone, Eq, PartialEq, Debug, Default)]
//! pub struct TestRuntime;
//!
//! use frame_system as system;
//! impl_outer_origin! {
//! pub enum Origin for TestRuntime {}
//! }
//!
//! impl frame_system::Config for TestRuntime {
//! ..
//! // we only care about these two for now. The rest can be mock. The block number type of
//! // kusama is u32.
//! type BlockNumber = u32;
//! type Header = Header;
//! ..
//! }
//!
//! #[test]
//! fn test_runtime_works() {
//! let hash: Hash =
//! hex!["f9a4ce984129569f63edc01b1c13374779f9384f1befd39931ffdcc83acf63a7"].into();
//! let parent: Hash =
//! hex!["540922e96a8fcaf945ed23c6f09c3e189bd88504ec945cc2171deaebeaf2f37e"].into();
//! Builder::new()
//! .at(hash)
//! .module("System")
//! .build()
//! .execute_with(|| {
//! assert_eq!(
//! // note: the hash corresponds to 3098546. We can check only the parent.
//! // https://polkascan.io/kusama/block/3098546
//! <frame_system::Module<Runtime>>::block_hash(3098545u32),
//! parent,
//! )
//! });
//! }
//! ```
//!
//! Or with the real kusama runtime.
//!
//! ```ignore
//! use remote_externalities::Builder;
//! use kusama_runtime::Runtime;
//!
//! #[test]
//! fn test_runtime_works() {
//! let hash: Hash =
//! hex!["f9a4ce984129569f63edc01b1c13374779f9384f1befd39931ffdcc83acf63a7"].into();
//! Builder::new()
//! .at(hash)
//! .module("Staking")
//! .build()
//! .execute_with(|| assert_eq!(<pallet_staking::Module<Runtime>>::validator_count(), 400));
//! }
//! ```
use std::{
fs,
path::{Path, PathBuf},
};
use log::*;
use sp_core::{hashing::twox_128};
pub use sp_io::TestExternalities;
use sp_core::{
hexdisplay::HexDisplay,
storage::{StorageKey, StorageData},
};
use futures::future::Future;
type KeyPair = (StorageKey, StorageData);
type Number = u32;
type Hash = sp_core::H256;
// TODO: make these two generic.
const LOG_TARGET: &'static str = "remote-ext";
/// The execution mode.
#[derive(Clone)]
pub enum Mode {
/// Online.
Online(OnlineConfig),
/// Offline. Uses a cached file and needs not any client config.
Offline(OfflineConfig),
}
/// configuration of the online execution.
///
/// A cache config must be present.
#[derive(Clone)]
pub struct OfflineConfig {
/// The configuration of the cache file to use. It must be present.
pub cache: CacheConfig,
}
/// Configuration of the online execution.
///
/// A cache config may be present and will be written to in that case.
#[derive(Clone)]
pub struct OnlineConfig {
/// The HTTP uri to use.
pub uri: String,
/// The block number at which to connect. Will be latest finalized head if not provided.
pub at: Option<Hash>,
/// An optional cache file to WRITE to, not for reading. Not cached if set to `None`.
pub cache: Option<CacheConfig>,
/// The modules to scrape. If empty, entire chain state will be scraped.
pub modules: Vec<String>,
}
impl Default for OnlineConfig {
fn default() -> Self {
Self {
uri: "http://localhost:9933".into(),
at: None,
cache: None,
modules: Default::default(),
}
}
}
/// Configuration of the cache.
#[derive(Clone)]
pub struct CacheConfig {
// TODO: I could mix these two into one filed, but I think separate is better bc one can be
// configurable while one not.
/// File name.
pub name: String,
/// Base directory.
pub directory: String,
}
impl Default for CacheConfig {
fn default() -> Self {
Self { name: "CACHE".into(), directory: ".".into() }
}
}
impl CacheConfig {
fn path(&self) -> PathBuf {
Path::new(&self.directory).join(self.name.clone())
}
}
/// Builder for remote-externalities.
pub struct Builder {
inject: Vec<KeyPair>,
mode: Mode,
chain: String,
}
impl Default for Builder {
fn default() -> Self {
Self {
inject: Default::default(),
mode: Mode::Online(OnlineConfig {
at: None,
uri: "http://localhost:9933".into(),
cache: None,
modules: Default::default(),
}),
chain: "UNSET".into(),
}
}
}
// Mode methods
impl Builder {
fn as_online(&self) -> &OnlineConfig {
match &self.mode {
Mode::Online(config) => &config,
_ => panic!("Unexpected mode: Online"),
}
}
fn as_online_mut(&mut self) -> &mut OnlineConfig {
match &mut self.mode {
Mode::Online(config) => config,
_ => panic!("Unexpected mode: Online"),
}
}
}
// RPC methods
impl Builder {
async fn rpc_get_head(&self) -> Hash {
let mut rt = tokio::runtime::Runtime::new().expect("Unable to create a runtime");
let uri = self.as_online().uri.clone();
rt.block_on::<_, _, ()>(futures::lazy(move || {
trace!(target: LOG_TARGET, "rpc: finalized_head");
let client: sc_rpc_api::chain::ChainClient<Number, Hash, (), ()> =
jsonrpc_core_client::transports::http::connect(&uri).wait().unwrap();
Ok(client.finalized_head().wait().unwrap())
}))
.unwrap()
}
/// Relay the request to `state_getPairs` rpc endpoint.
///
/// Note that this is an unsafe RPC.
async fn rpc_get_pairs(&self, prefix: StorageKey, at: Hash) -> Vec<KeyPair> {
let mut rt = tokio::runtime::Runtime::new().expect("Unable to create a runtime");
let uri = self.as_online().uri.clone();
rt.block_on::<_, _, ()>(futures::lazy(move || {
trace!(target: LOG_TARGET, "rpc: storage_pairs: {:?} / {:?}", prefix, at);
let client: sc_rpc_api::state::StateClient<Hash> =
jsonrpc_core_client::transports::http::connect(&uri).wait().unwrap();
Ok(client.storage_pairs(prefix, Some(at)).wait().unwrap())
}))
.unwrap()
}
/// Get the chain name.
async fn chain_name(&self) -> String {
let mut rt = tokio::runtime::Runtime::new().expect("Unable to create a runtime");
let uri = self.as_online().uri.clone();
rt.block_on::<_, _, ()>(futures::lazy(move || {
trace!(target: LOG_TARGET, "rpc: system_chain");
let client: sc_rpc_api::system::SystemClient<(), ()> =
jsonrpc_core_client::transports::http::connect(&uri).wait().unwrap();
Ok(client.system_chain().wait().unwrap())
}))
.unwrap()
}
}
// Internal methods
impl Builder {
/// Save the given data as cache.
fn save_cache(&self, data: &[KeyPair], path: &Path) {
let bdata = bincode::serialize(data).unwrap();
info!(target: LOG_TARGET, "writing to cache file {:?}", path);
fs::write(path, bdata).unwrap();
}
/// initialize `Self` from cache. Panics if the file does not exist.
fn load_cache(&self, path: &Path) -> Vec<KeyPair> {
info!(target: LOG_TARGET, "scraping keypairs from cache {:?}", path,);
let bytes = fs::read(path).unwrap();
bincode::deserialize(&bytes[..]).unwrap()
}
/// Build `Self` from a network node denoted by `uri`.
async fn load_remote(&self) -> Vec<KeyPair> {
let config = self.as_online();
let at = self.as_online().at.unwrap().clone();
info!(target: LOG_TARGET, "scraping keypairs from remote node {} @ {:?}", config.uri, at);
let keys_and_values = if config.modules.len() > 0 {
let mut filtered_kv = vec![];
for f in config.modules.iter() {
let hashed_prefix = StorageKey(twox_128(f.as_bytes()).to_vec());
let module_kv = self.rpc_get_pairs(hashed_prefix.clone(), at).await;
info!(
target: LOG_TARGET,
"downloaded data for module {} (count: {} / prefix: {:?}).",
f,
module_kv.len(),
HexDisplay::from(&hashed_prefix),
);
filtered_kv.extend(module_kv);
}
filtered_kv
} else {
info!(target: LOG_TARGET, "downloading data for all modules.");
self.rpc_get_pairs(StorageKey(vec![]), at).await.into_iter().collect::<Vec<_>>()
};
keys_and_values
}
async fn init_remote_client(&mut self) {
self.as_online_mut().at = Some(self.rpc_get_head().await);
self.chain = self.chain_name().await;
}
async fn pre_build(mut self) -> Vec<KeyPair> {
let mut base_kv = match self.mode.clone() {
Mode::Offline(config) => self.load_cache(&config.cache.path()),
Mode::Online(config) => {
self.init_remote_client().await;
let kp = self.load_remote().await;
if let Some(c) = config.cache {
self.save_cache(&kp, &c.path());
}
kp
}
};
info!(
target: LOG_TARGET,
"extending externalities with {} manually injected keys",
self.inject.len()
);
base_kv.extend(self.inject.clone());
base_kv
}
}
// Public methods
impl Builder {
/// Create a new builder.
pub fn new() -> Self {
Default::default()
}
/// Inject a manual list of key and values to the storage.
pub fn inject(mut self, injections: &[KeyPair]) -> Self {
for i in injections {
self.inject.push(i.clone());
}
self
}
/// Configure a cache to be used.
pub fn mode(mut self, mode: Mode) -> Self {
self.mode = mode;
self
}
/// Build the test externalities.
pub async fn build(self) -> TestExternalities {
let kv = self.pre_build().await;
let mut ext = TestExternalities::new_empty();
info!(target: LOG_TARGET, "injecting a total of {} keys", kv.len());
for (k, v) in kv {
let (k, v) = (k.0, v.0);
ext.insert(k, v);
}
ext
}
}
#[cfg(test)]
mod tests {
use super::*;
fn init_logger() {
let _ = env_logger::Builder::from_default_env()
.format_module_path(false)
.format_level(true)
.try_init();
}
#[async_std::test]
#[cfg(feature = "remote-test")]
async fn can_build_one_pallet() {
init_logger();
Builder::new()
.mode(Mode::Online(OnlineConfig {
modules: vec!["Proxy".into()],
..Default::default()
}))
.build()
.await
.execute_with(|| {});
}
#[async_std::test]
async fn can_load_cache() {
init_logger();
Builder::new()
.mode(Mode::Offline(OfflineConfig {
cache: CacheConfig { name: "proxy_test".into(), ..Default::default() },
}))
.build()
.await
.execute_with(|| {});
}
#[async_std::test]
#[cfg(feature = "remote-test")]
async fn can_create_cache() {
init_logger();
Builder::new()
.mode(Mode::Online(OnlineConfig {
cache: Some(CacheConfig {
name: "test_cache_to_remove.bin".into(),
..Default::default()
}),
..Default::default()
}))
.build()
.await
.execute_with(|| {});
let to_delete = std::fs::read_dir(CacheConfig::default().directory)
.unwrap()
.into_iter()
.map(|d| d.unwrap())
.filter(|p| p.path().extension().unwrap_or_default() == "bin")
.collect::<Vec<_>>();
assert!(to_delete.len() > 0);
for d in to_delete {
std::fs::remove_file(d.path()).unwrap();
}
}
#[async_std::test]
#[cfg(feature = "remote-test")]
async fn can_build_all() {
init_logger();
Builder::new().build().await.execute_with(|| {});
}
}
@@ -0,0 +1,32 @@
[package]
name = "try-runtime-cli"
version = "0.9.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
license = "Apache-2.0"
homepage = "https://substrate.dev"
repository = "https://github.com/paritytech/substrate/"
description = "Cli command runtime testing and dry-running"
readme = "README.md"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
log = "0.4.8"
parity-scale-codec = { version = "2.0.0" }
sc-service = { version = "0.9.0", default-features = false, path = "../../../../client/service" }
sc-cli = { version = "0.9.0", path = "../../../../client/cli" }
sc-executor = { path = "../../../../client/executor" }
sc-client-api = { version = "3.0.0", path = "../../../../client/api" }
structopt = "0.3.8"
sp-state-machine = { version = "0.9.0", path = "../../../../primitives/state-machine" }
sp-api = { version = "3.0.0", path = "../../../../primitives/api" }
sp-blockchain = { version = "3.0.0", path = "../../../../primitives/blockchain" }
sp-runtime = { version = "3.0.0", path = "../../../../primitives/runtime" }
sp-externalities = { version = "0.9.0", path = "../../../../primitives/externalities" }
sp-core = { version = "3.0.0", path = "../../../../primitives/core" }
frame-try-runtime = { version = "0.9.0", path = "../../../../frame/try-runtime" }
remote-externalities = { path = "../../remote-externalities" }
@@ -0,0 +1,178 @@
// This file is part of Substrate.
// Copyright (C) 2021 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! `Structopt`-ready struct for `try-runtime`.
use parity_scale_codec::Decode;
use std::{fmt::Debug, str::FromStr};
use sc_service::Configuration;
use sc_cli::{CliConfiguration, ExecutionStrategy, WasmExecutionMethod};
use sc_executor::NativeExecutor;
use sc_service::NativeExecutionDispatch;
use sp_state_machine::StateMachine;
use sp_runtime::traits::{Block as BlockT, NumberFor};
use sp_core::storage::{StorageData, StorageKey, well_known_keys};
/// Various commands to try out the new runtime, over configurable states.
///
/// For now this only assumes running the `on_runtime_upgrade` hooks.
#[derive(Debug, structopt::StructOpt)]
pub struct TryRuntimeCmd {
/// The shared parameters
#[allow(missing_docs)]
#[structopt(flatten)]
pub shared_params: sc_cli::SharedParams,
/// The state to use to run the migration. Should be a valid FILE or HTTP URI.
#[structopt(short, long, default_value = "http://localhost:9933")]
pub state: State,
/// The execution strategy that should be used for benchmarks
#[structopt(
long = "execution",
value_name = "STRATEGY",
possible_values = &ExecutionStrategy::variants(),
case_insensitive = true,
default_value = "Native",
)]
pub execution: ExecutionStrategy,
/// Method for executing Wasm runtime code.
#[structopt(
long = "wasm-execution",
value_name = "METHOD",
possible_values = &WasmExecutionMethod::enabled_variants(),
case_insensitive = true,
default_value = "Interpreted"
)]
pub wasm_method: WasmExecutionMethod,
}
/// The state to use for a migration dry-run.
#[derive(Debug)]
pub enum State {
/// A snapshot. Inner value is a file path.
Snap(String),
/// A live chain. Inner value is the HTTP uri.
Live(String),
}
impl FromStr for State {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.get(..7) {
// could use Url crate as well, but lets keep it simple for now.
Some("http://") => Ok(State::Live(s.to_string())),
Some("file://") => s
.split("//")
.collect::<Vec<_>>()
.get(1)
.map(|s| State::Snap(s.to_string()))
.ok_or("invalid file URI"),
_ => Err("invalid format. Must be a valid HTTP or File URI"),
}
}
}
impl TryRuntimeCmd {
pub async fn run<B, ExecDispatch>(&self, config: Configuration) -> sc_cli::Result<()>
where
B: BlockT,
ExecDispatch: NativeExecutionDispatch + 'static,
{
let spec = config.chain_spec;
let genesis_storage = spec.build_storage()?;
let code = StorageData(
genesis_storage
.top
.get(well_known_keys::CODE)
.expect("code key must exist in genesis storage; qed")
.to_vec(),
);
let code_key = StorageKey(well_known_keys::CODE.to_vec());
let wasm_method = self.wasm_method;
let execution = self.execution;
let mut changes = Default::default();
// don't really care about these -- use the default values.
let max_runtime_instances = config.max_runtime_instances;
let heap_pages = config.default_heap_pages;
let executor = NativeExecutor::<ExecDispatch>::new(
wasm_method.into(),
heap_pages,
max_runtime_instances,
);
let ext = {
use remote_externalities::{Builder, Mode, CacheConfig, OfflineConfig, OnlineConfig};
let builder = match &self.state {
State::Snap(file_path) => Builder::new().mode(Mode::Offline(OfflineConfig {
cache: CacheConfig { name: file_path.into(), ..Default::default() },
})),
State::Live(http_uri) => Builder::new().mode(Mode::Online(OnlineConfig {
uri: http_uri.into(),
..Default::default()
})),
};
// inject the code into this ext.
builder.inject(&[(code_key, code)]).build().await
};
let encoded_result = StateMachine::<_, _, NumberFor<B>, _>::new(
&ext.backend,
None,
&mut changes,
&executor,
"TryRuntime_on_runtime_upgrade",
&[],
ext.extensions,
&sp_state_machine::backend::BackendRuntimeCode::new(&ext.backend)
.runtime_code()?,
sp_core::testing::TaskExecutor::new(),
)
.execute(execution.into())
.map_err(|e| format!("failed to execute 'TryRuntime_on_runtime_upgrade' due to {:?}", e))?;
let (weight, total_weight) = <(u64, u64) as Decode>::decode(&mut &*encoded_result)
.map_err(|e| format!("failed to decode output due to {:?}", e))?;
log::info!(
"try-runtime executed without errors. Consumed weight = {}, total weight = {} ({})",
weight,
total_weight,
weight as f64 / total_weight as f64
);
Ok(())
}
}
impl CliConfiguration for TryRuntimeCmd {
fn shared_params(&self) -> &sc_cli::SharedParams {
&self.shared_params
}
fn chain_id(&self, _is_dev: bool) -> sc_cli::Result<String> {
Ok(match self.shared_params.chain {
Some(ref chain) => chain.clone(),
None => "dev".into(),
})
}
}