From 10f2a2acf8b5ae51d7c440e6bbd21209e578f494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Fri, 17 Feb 2023 13:21:58 +0100 Subject: [PATCH] `try-runtime::fast-forward` (#12896) * try-runtime::fast-forward * Revert un`pub`ing command's fields * Handle storage change failure * Adjust Substrate node * Feature-gated imports * doc link * Feature-gated imports in node-template * Move trait, blanket implementation and auxiliary functions to a new module * Distinguish between plain babe+timestamp and substrate enhanced info * Remove uncles inherents * Missing argument * Add doc comment about `blocktime_millis` * Add licenses --- Cargo.lock | 6 + bin/node-template/node/src/command.rs | 7 +- bin/node/cli/src/command.rs | 10 +- utils/frame/try-runtime/cli/Cargo.toml | 6 + .../cli/src/block_building_info.rs | 152 ++++++++++ .../cli/src/commands/fast_forward.rs | 268 ++++++++++++++++++ .../frame/try-runtime/cli/src/commands/mod.rs | 1 + utils/frame/try-runtime/cli/src/lib.rs | 27 +- 8 files changed, 473 insertions(+), 4 deletions(-) create mode 100644 utils/frame/try-runtime/cli/src/block_building_info.rs create mode 100644 utils/frame/try-runtime/cli/src/commands/fast_forward.rs diff --git a/Cargo.lock b/Cargo.lock index b33f7627f230e..d7f48c17f8726 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11426,6 +11426,7 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" name = "try-runtime-cli" version = "0.10.0-dev" dependencies = [ + "async-trait", "clap 4.1.4", "frame-remote-externalities", "frame-try-runtime", @@ -11438,14 +11439,19 @@ dependencies = [ "serde", "serde_json", "sp-api", + "sp-consensus-aura", + "sp-consensus-babe", "sp-core", "sp-debug-derive", "sp-externalities", + "sp-inherents", "sp-io", "sp-keystore", "sp-rpc", "sp-runtime", "sp-state-machine", + "sp-timestamp", + "sp-transaction-storage-proof", "sp-version", "sp-weights", "substrate-rpc-client", diff --git a/bin/node-template/node/src/command.rs b/bin/node-template/node/src/command.rs index 15cd69b34b5b2..c3dc098cdfb3a 100644 --- a/bin/node-template/node/src/command.rs +++ b/bin/node-template/node/src/command.rs @@ -10,6 +10,9 @@ use sc_cli::{ChainSpec, RuntimeVersion, SubstrateCli}; use sc_service::PartialComponents; use sp_keyring::Sr25519Keyring; +#[cfg(feature = "try-runtime")] +use try_runtime_cli::block_building_info::timestamp_with_aura_info; + impl SubstrateCli for Cli { fn impl_name() -> String { "Substrate Node".into() @@ -184,11 +187,13 @@ pub fn run() -> sc_cli::Result<()> { let task_manager = sc_service::TaskManager::new(config.tokio_handle.clone(), registry) .map_err(|e| sc_cli::Error::Service(sc_service::Error::Prometheus(e)))?; + let info_provider = timestamp_with_aura_info(6000); + Ok(( cmd.run::::ExtendHostFunctions, - >>(), + >, _>(Some(info_provider)), task_manager, )) }) diff --git a/bin/node/cli/src/command.rs b/bin/node/cli/src/command.rs index 2ed8a2c754018..4256c7d61f4fd 100644 --- a/bin/node/cli/src/command.rs +++ b/bin/node/cli/src/command.rs @@ -32,6 +32,12 @@ use sp_keyring::Sr25519Keyring; use std::sync::Arc; +#[cfg(feature = "try-runtime")] +use { + kitchensink_runtime::constants::time::SLOT_DURATION, + try_runtime_cli::block_building_info::substrate_info, +}; + impl SubstrateCli for Cli { fn impl_name() -> String { "Substrate Node".into() @@ -236,11 +242,13 @@ pub fn run() -> Result<()> { sc_service::TaskManager::new(config.tokio_handle.clone(), registry) .map_err(|e| sc_cli::Error::Service(sc_service::Error::Prometheus(e)))?; + let info_provider = substrate_info(SLOT_DURATION); + Ok(( cmd.run::::ExtendHostFunctions, - >>(), + >, _>(Some(info_provider)), task_manager, )) }) diff --git a/utils/frame/try-runtime/cli/Cargo.toml b/utils/frame/try-runtime/cli/Cargo.toml index 8c14ddc2916b0..a220289542464 100644 --- a/utils/frame/try-runtime/cli/Cargo.toml +++ b/utils/frame/try-runtime/cli/Cargo.toml @@ -16,13 +16,18 @@ remote-externalities = { version = "0.10.0-dev", path = "../../remote-externalit sc-cli = { version = "0.10.0-dev", path = "../../../../client/cli" } sc-executor = { version = "0.10.0-dev", path = "../../../../client/executor" } sc-service = { version = "0.10.0-dev", default-features = false, path = "../../../../client/service" } +sp-consensus-aura = { path = "../../../../primitives/consensus/aura" } +sp-consensus-babe = { path = "../../../../primitives/consensus/babe" } sp-core = { version = "7.0.0", path = "../../../../primitives/core" } sp-externalities = { version = "0.13.0", path = "../../../../primitives/externalities" } +sp-inherents = { path = "../../../../primitives/inherents" } sp-io = { version = "7.0.0", path = "../../../../primitives/io" } sp-keystore = { version = "0.13.0", path = "../../../../primitives/keystore" } sp-runtime = { version = "7.0.0", path = "../../../../primitives/runtime" } sp-rpc = { version = "6.0.0", path = "../../../../primitives/rpc" } sp-state-machine = { version = "0.13.0", path = "../../../../primitives/state-machine" } +sp-timestamp = { path = "../../../../primitives/timestamp" } +sp-transaction-storage-proof = { path = "../../../../primitives/transaction-storage-proof" } sp-version = { version = "5.0.0", path = "../../../../primitives/version" } sp-debug-derive = { path = "../../../../primitives/debug-derive" } sp-api = { path = "../../../../primitives/api" } @@ -30,6 +35,7 @@ sp-weights = { version = "4.0.0", path = "../../../../primitives/weights" } frame-try-runtime = { optional = true, path = "../../../../frame/try-runtime" } substrate-rpc-client = { path = "../../rpc/client" } +async-trait = "0.1.57" clap = { version = "4.0.9", features = ["derive"] } hex = { version = "0.4.3", default-features = false } log = "0.4.17" diff --git a/utils/frame/try-runtime/cli/src/block_building_info.rs b/utils/frame/try-runtime/cli/src/block_building_info.rs new file mode 100644 index 0000000000000..b68bf081fee3e --- /dev/null +++ b/utils/frame/try-runtime/cli/src/block_building_info.rs @@ -0,0 +1,152 @@ +// This file is part of Substrate. + +// Copyright (C) 2021-2022 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. + +use crate::BlockT; +use parity_scale_codec::Encode; +use sc_cli::Result; +use sp_consensus_aura::{Slot, SlotDuration, AURA_ENGINE_ID}; +use sp_consensus_babe::{ + digests::{PreDigest, SecondaryPlainPreDigest}, + BABE_ENGINE_ID, +}; +use sp_inherents::{InherentData, InherentDataProvider}; +use sp_runtime::{Digest, DigestItem}; +use sp_timestamp::TimestampInherentData; + +/// Something that can create inherent data providers and pre-runtime digest. +/// +/// It is possible for the caller to provide custom arguments to the callee by setting the +/// `ExtraArgs` generic parameter. +/// +/// This module already provides some convenience implementation of this trait for closures. So, it +/// should not be required to implement it directly. +#[async_trait::async_trait] +pub trait BlockBuildingInfoProvider { + type InherentDataProviders: InherentDataProvider; + + async fn get_inherent_providers_and_pre_digest( + &self, + parent_hash: Block::Hash, + extra_args: ExtraArgs, + ) -> Result<(Self::InherentDataProviders, Vec)>; +} + +#[async_trait::async_trait] +impl BlockBuildingInfoProvider for F +where + Block: BlockT, + F: Fn(Block::Hash, ExtraArgs) -> Fut + Sync + Send, + Fut: std::future::Future)>> + Send + 'static, + IDP: InherentDataProvider + 'static, + ExtraArgs: Send + 'static, +{ + type InherentDataProviders = IDP; + + async fn get_inherent_providers_and_pre_digest( + &self, + parent: Block::Hash, + extra_args: ExtraArgs, + ) -> Result<(Self::InherentDataProviders, Vec)> { + (*self)(parent, extra_args).await + } +} + +/// Provides [`BlockBuildingInfoProvider`] implementation for chains that include timestamp inherent +/// and use Aura for a block production. +/// +/// It depends only on the expected block production frequency, i.e. `blocktime_millis`. +pub fn timestamp_with_aura_info( + blocktime_millis: u64, +) -> impl BlockBuildingInfoProvider> { + move |_, maybe_prev_info: Option<(InherentData, Digest)>| async move { + let timestamp_idp = match maybe_prev_info { + Some((inherent_data, _)) => sp_timestamp::InherentDataProvider::new( + inherent_data.timestamp_inherent_data().unwrap().unwrap() + blocktime_millis, + ), + None => sp_timestamp::InherentDataProvider::from_system_time(), + }; + + let slot = + Slot::from_timestamp(*timestamp_idp, SlotDuration::from_millis(blocktime_millis)); + let digest = vec![DigestItem::PreRuntime(AURA_ENGINE_ID, slot.encode())]; + + Ok((timestamp_idp, digest)) + } +} + +/// Provides [`BlockBuildingInfoProvider`] implementation for chains that include timestamp inherent +/// and use Babe for a block production. +/// +/// It depends only on the expected block production frequency, i.e. `blocktime_millis`. +pub fn timestamp_with_babe_info( + blocktime_millis: u64, +) -> impl BlockBuildingInfoProvider> { + move |_, maybe_prev_info: Option<(InherentData, Digest)>| async move { + let timestamp_idp = match maybe_prev_info { + Some((inherent_data, _)) => sp_timestamp::InherentDataProvider::new( + inherent_data.timestamp_inherent_data().unwrap().unwrap() + blocktime_millis, + ), + None => sp_timestamp::InherentDataProvider::from_system_time(), + }; + + let slot = + Slot::from_timestamp(*timestamp_idp, SlotDuration::from_millis(blocktime_millis)); + let slot_idp = sp_consensus_babe::inherents::InherentDataProvider::new(slot); + + let digest = vec![DigestItem::PreRuntime( + BABE_ENGINE_ID, + PreDigest::SecondaryPlain(SecondaryPlainPreDigest { slot, authority_index: 0 }) + .encode(), + )]; + + Ok(((slot_idp, timestamp_idp), digest)) + } +} + +/// Provides [`BlockBuildingInfoProvider`] implementation for chains that use: +/// - timestamp inherent, +/// - Babe for a block production (inherent + digest), +/// - uncles inherent, +/// - storage proof inherent +/// +/// It depends only on the expected block production frequency, i.e. `blocktime_millis`. +pub fn substrate_info( + blocktime_millis: u64, +) -> impl BlockBuildingInfoProvider> { + move |_, maybe_prev_info: Option<(InherentData, Digest)>| async move { + let timestamp_idp = match maybe_prev_info { + Some((inherent_data, _)) => sp_timestamp::InherentDataProvider::new( + inherent_data.timestamp_inherent_data().unwrap().unwrap() + blocktime_millis, + ), + None => sp_timestamp::InherentDataProvider::from_system_time(), + }; + + let slot = + Slot::from_timestamp(*timestamp_idp, SlotDuration::from_millis(blocktime_millis)); + let slot_idp = sp_consensus_babe::inherents::InherentDataProvider::new(slot); + + let storage_proof_idp = sp_transaction_storage_proof::InherentDataProvider::new(None); + + let digest = vec![DigestItem::PreRuntime( + BABE_ENGINE_ID, + PreDigest::SecondaryPlain(SecondaryPlainPreDigest { slot, authority_index: 0 }) + .encode(), + )]; + + Ok(((slot_idp, timestamp_idp, storage_proof_idp), digest)) + } +} diff --git a/utils/frame/try-runtime/cli/src/commands/fast_forward.rs b/utils/frame/try-runtime/cli/src/commands/fast_forward.rs new file mode 100644 index 0000000000000..ae8f9a5cf17f2 --- /dev/null +++ b/utils/frame/try-runtime/cli/src/commands/fast_forward.rs @@ -0,0 +1,268 @@ +// This file is part of Substrate. + +// Copyright (C) 2021-2022 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. + +use crate::{ + block_building_info::BlockBuildingInfoProvider, build_executor, full_extensions, + rpc_err_handler, state_machine_call, BlockT, LiveState, SharedParams, State, +}; +use parity_scale_codec::{Decode, Encode}; +use sc_cli::Result; +use sc_executor::{sp_wasm_interface::HostFunctions, WasmExecutor}; +use serde::de::DeserializeOwned; +use sp_core::H256; +use sp_inherents::{InherentData, InherentDataProvider}; +use sp_io::TestExternalities; +use sp_runtime::{ + traits::{Header, NumberFor, One}, + Digest, +}; +use std::{fmt::Debug, str::FromStr}; +use substrate_rpc_client::{ws_client, ChainApi}; + +/// Configurations of the [`crate::Command::FastForward`]. +#[derive(Debug, Clone, clap::Parser)] +pub struct FastForwardCmd { + /// How many blocks should be processed. If `None`, then blocks will be produced and processed + /// in a loop. + #[arg(long)] + n_blocks: Option, + + /// The state type to use. + #[command(subcommand)] + state: State, + + /// The ws uri from which to fetch the block. + /// + /// If `state` is `Live`, this is ignored. Otherwise, it must not be empty. + #[arg(long, value_parser = crate::parse::url)] + block_ws_uri: Option, + + /// Which try-state targets to execute when running this command. + /// + /// Expected values: + /// - `all` + /// - `none` + /// - A comma separated list of pallets, as per pallet names in `construct_runtime!()` (e.g. + /// `Staking, System`). + /// - `rr-[x]` where `[x]` is a number. Then, the given number of pallets are checked in a + /// round-robin fashion. + #[arg(long, default_value = "all")] + try_state: frame_try_runtime::TryStateSelect, +} + +impl FastForwardCmd { + fn block_ws_uri(&self) -> &str { + match self.state { + State::Live(LiveState { ref uri, .. }) => &uri, + _ => self + .block_ws_uri + .as_ref() + .expect("Either `--block-uri` must be provided, or state must be `live`"), + } + } +} + +/// Read the block number corresponding to `hash` with an RPC call to `ws_uri`. +async fn get_block_number( + hash: Block::Hash, + ws_uri: &str, +) -> Result> +where + Block::Header: DeserializeOwned, +{ + let rpc = ws_client(ws_uri).await?; + Ok(ChainApi::<(), Block::Hash, Block::Header, ()>::header(&rpc, Some(hash)) + .await + .map_err(rpc_err_handler) + .and_then(|maybe_header| maybe_header.ok_or("header_not_found").map(|h| *h.number()))?) +} + +/// Call `method` with `data` and return the result. `externalities` will not change. +async fn dry_run( + externalities: &TestExternalities, + executor: &WasmExecutor, + method: &'static str, + data: &[u8], +) -> Result { + let (_, result) = state_machine_call::( + externalities, + executor, + method, + data, + full_extensions(), + )?; + + Ok(::decode(&mut &*result)?) +} + +/// Call `method` with `data` and actually save storage changes to `externalities`. +async fn run( + externalities: &mut TestExternalities, + executor: &WasmExecutor, + method: &'static str, + data: &[u8], +) -> Result<()> { + let (mut changes, _) = state_machine_call::( + externalities, + executor, + method, + data, + full_extensions(), + )?; + + let storage_changes = changes.drain_storage_changes( + &externalities.backend, + &mut Default::default(), + externalities.state_version, + )?; + + externalities + .backend + .apply_transaction(storage_changes.transaction_storage_root, storage_changes.transaction); + + Ok(()) +} + +/// Produce next empty block. +async fn next_empty_block< + Block: BlockT, + HostFns: HostFunctions, + BBIP: BlockBuildingInfoProvider>, +>( + externalities: &mut TestExternalities, + executor: &WasmExecutor, + parent_height: NumberFor, + parent_hash: Block::Hash, + block_building_info_provider: &Option, + previous_block_building_info: Option<(InherentData, Digest)>, +) -> Result<(Block, Option<(InherentData, Digest)>)> { + let (maybe_inherent_data, pre_digest) = match &block_building_info_provider { + None => (None, Default::default()), + Some(bbip) => { + let (inherent_data_provider, pre_digest) = bbip + .get_inherent_providers_and_pre_digest(parent_hash, previous_block_building_info) + .await?; + let inherent_data = inherent_data_provider + .create_inherent_data() + .await + .map_err(|e| sc_cli::Error::Input(format!("I don't know how to convert {e:?}")))?; + + (Some(inherent_data), Digest { logs: pre_digest }) + }, + }; + + let header = Block::Header::new( + parent_height + One::one(), + Default::default(), + Default::default(), + parent_hash, + pre_digest.clone(), + ); + let mut extrinsics = >::new(); + + run::(externalities, executor, "Core_initialize_block", &header.encode()).await?; + + if let Some(ref inherent_data) = maybe_inherent_data { + extrinsics = dry_run::, Block, _>( + externalities, + executor, + "BlockBuilder_inherent_extrinsics", + &inherent_data.encode(), + ) + .await?; + } + + for xt in &extrinsics { + run::(externalities, executor, "BlockBuilder_apply_extrinsic", &xt.encode()) + .await?; + } + + let header = dry_run::( + externalities, + executor, + "BlockBuilder_finalize_block", + &[0u8; 0], + ) + .await?; + + run::(externalities, executor, "BlockBuilder_finalize_block", &[0u8; 0]).await?; + + Ok((Block::new(header, extrinsics), (maybe_inherent_data.map(|id| (id, pre_digest))))) +} + +pub(crate) async fn fast_forward( + shared: SharedParams, + command: FastForwardCmd, + block_building_info_provider: Option, +) -> Result<()> +where + Block: BlockT + DeserializeOwned, + Block::Hash: FromStr, + Block::Header: DeserializeOwned, + ::Err: Debug, + NumberFor: FromStr, + as FromStr>::Err: Debug, + HostFns: HostFunctions, + BBIP: BlockBuildingInfoProvider>, +{ + let executor = build_executor::(&shared); + let ext = command.state.into_ext::(&shared, &executor, None, true).await?; + + let mut last_block_hash = ext.block_hash; + let mut last_block_number = + get_block_number::(last_block_hash, command.block_ws_uri()).await?; + let mut prev_block_building_info = None; + + let mut ext = ext.inner_ext; + + for _ in 1..=command.n_blocks.unwrap_or(u64::MAX) { + // We are saving state before we overwrite it while producing new block. + let backend = ext.as_backend(); + + log::info!("Producing new empty block at height {:?}", last_block_number + One::one()); + + let (next_block, new_block_building_info) = next_empty_block::( + &mut ext, + &executor, + last_block_number, + last_block_hash, + &block_building_info_provider, + prev_block_building_info, + ) + .await?; + + log::info!("Produced a new block: {:?}", next_block.header()); + + // And now we restore previous state. + ext.backend = backend; + + let state_root_check = true; + let signature_check = true; + let payload = + (next_block.clone(), state_root_check, signature_check, command.try_state.clone()) + .encode(); + run::(&mut ext, &executor, "TryRuntime_execute_block", &payload).await?; + + log::info!("Executed the new block"); + + prev_block_building_info = new_block_building_info; + last_block_hash = next_block.hash(); + last_block_number += One::one(); + } + + Ok(()) +} diff --git a/utils/frame/try-runtime/cli/src/commands/mod.rs b/utils/frame/try-runtime/cli/src/commands/mod.rs index ab0a066585f6a..81deb263163b0 100644 --- a/utils/frame/try-runtime/cli/src/commands/mod.rs +++ b/utils/frame/try-runtime/cli/src/commands/mod.rs @@ -17,6 +17,7 @@ pub mod create_snapshot; pub mod execute_block; +pub mod fast_forward; pub mod follow_chain; pub mod offchain_worker; pub mod on_runtime_upgrade; diff --git a/utils/frame/try-runtime/cli/src/lib.rs b/utils/frame/try-runtime/cli/src/lib.rs index dea20febb9c58..1a2f32de52ec1 100644 --- a/utils/frame/try-runtime/cli/src/lib.rs +++ b/utils/frame/try-runtime/cli/src/lib.rs @@ -358,6 +358,7 @@ #![cfg(feature = "try-runtime")] +use crate::block_building_info::BlockBuildingInfoProvider; use parity_scale_codec::Decode; use remote_externalities::{ Builder, Mode, OfflineConfig, OnlineConfig, RemoteExternalities, SnapshotConfig, @@ -381,15 +382,17 @@ use sp_core::{ twox_128, H256, }; use sp_externalities::Extensions; +use sp_inherents::InherentData; use sp_keystore::{testing::KeyStore, KeystoreExt}; use sp_runtime::{ traits::{BlakeTwo256, Block as BlockT, NumberFor}, - DeserializeOwned, + DeserializeOwned, Digest, }; use sp_state_machine::{CompactProof, OverlayedChanges, StateMachine, TrieBackendBuilder}; use sp_version::StateVersion; use std::{fmt::Debug, path::PathBuf, str::FromStr}; +pub mod block_building_info; pub mod commands; pub(crate) mod parse; pub(crate) const LOG_TARGET: &str = "try-runtime::cli"; @@ -445,6 +448,15 @@ pub enum Command { /// tested has remained the same, otherwise block decoding might fail. FollowChain(commands::follow_chain::FollowChainCmd), + /// Produce a series of empty, consecutive blocks and execute them one-by-one. + /// + /// To compare it with [`Command::FollowChain`]: + /// - we don't have the delay of the original blocktime (for Polkadot 6s), but instead, we + /// execute every block immediately + /// - the only data that will be put into blocks are pre-runtime digest items and inherent + /// extrinsics; both things should be defined in your node CLI handling level + FastForward(commands::fast_forward::FastForwardCmd), + /// Create a new snapshot file. CreateSnapshot(commands::create_snapshot::CreateSnapshotCmd), } @@ -719,7 +731,10 @@ impl State { } impl TryRuntimeCmd { - pub async fn run(&self) -> sc_cli::Result<()> + pub async fn run( + &self, + block_building_info_provider: Option, + ) -> sc_cli::Result<()> where Block: BlockT + DeserializeOwned, Block::Header: DeserializeOwned, @@ -729,6 +744,7 @@ impl TryRuntimeCmd { as TryInto>::Error: Debug, NumberFor: FromStr, HostFns: HostFunctions, + BBIP: BlockBuildingInfoProvider>, { match &self.command { Command::OnRuntimeUpgrade(ref cmd) => @@ -755,6 +771,13 @@ impl TryRuntimeCmd { cmd.clone(), ) .await, + Command::FastForward(cmd) => + commands::fast_forward::fast_forward::( + self.shared.clone(), + cmd.clone(), + block_building_info_provider, + ) + .await, Command::CreateSnapshot(cmd) => commands::create_snapshot::create_snapshot::( self.shared.clone(),