#![cfg(feature = "try-runtime")]
use crate::block_building_info::BlockBuildingInfoProvider;
use parity_scale_codec::Decode;
use remote_externalities::{
Builder, Mode, OfflineConfig, OnlineConfig, RemoteExternalities, SnapshotConfig,
};
use sc_cli::{
execution_method_from_cli, CliConfiguration, RuntimeVersion, WasmExecutionMethod,
WasmtimeInstantiationStrategy, DEFAULT_WASMTIME_INSTANTIATION_STRATEGY,
DEFAULT_WASM_EXECUTION_METHOD,
};
use sc_executor::{
sp_wasm_interface::HostFunctions, HeapAllocStrategy, WasmExecutor, DEFAULT_HEAP_ALLOC_STRATEGY,
};
use sp_core::{
hexdisplay::HexDisplay,
offchain::{
testing::{TestOffchainExt, TestTransactionPoolExt},
OffchainDbExt, OffchainWorkerExt, TransactionPoolExt,
},
storage::well_known_keys,
traits::{CallContext, ReadRuntimeVersion, ReadRuntimeVersionExt},
twox_128, H256,
};
use sp_externalities::Extensions;
use sp_inherents::InherentData;
use sp_keystore::{testing::MemoryKeystore, KeystoreExt};
use sp_runtime::{
traits::{BlakeTwo256, Block as BlockT, Hash as HashT, HashingFor, NumberFor},
DeserializeOwned, Digest,
};
use sp_state_machine::{
CompactProof, OverlayedChanges, StateMachine, TestExternalities, 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";
#[derive(Debug, Clone, clap::Subcommand)]
pub enum Command {
OnRuntimeUpgrade(commands::on_runtime_upgrade::OnRuntimeUpgradeCmd),
ExecuteBlock(commands::execute_block::ExecuteBlockCmd),
OffchainWorker(commands::offchain_worker::OffchainWorkerCmd),
FollowChain(commands::follow_chain::FollowChainCmd),
FastForward(commands::fast_forward::FastForwardCmd),
CreateSnapshot(commands::create_snapshot::CreateSnapshotCmd),
}
#[derive(Debug, Clone)]
pub enum Runtime {
Path(PathBuf),
Existing,
}
impl FromStr for Runtime {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.to_lowercase().as_ref() {
"existing" => Runtime::Existing,
x @ _ => Runtime::Path(x.into()),
})
}
}
#[derive(Debug, Clone, clap::Parser)]
#[group(skip)]
pub struct SharedParams {
#[allow(missing_docs)]
#[clap(flatten)]
pub shared_params: sc_cli::SharedParams,
#[arg(long)]
pub runtime: Runtime,
#[arg(
long = "wasm-execution",
value_name = "METHOD",
value_enum,
ignore_case = true,
default_value_t = DEFAULT_WASM_EXECUTION_METHOD,
)]
pub wasm_method: WasmExecutionMethod,
#[arg(
long = "wasm-instantiation-strategy",
value_name = "STRATEGY",
default_value_t = DEFAULT_WASMTIME_INSTANTIATION_STRATEGY,
value_enum,
)]
pub wasmtime_instantiation_strategy: WasmtimeInstantiationStrategy,
#[arg(long)]
pub heap_pages: Option<u64>,
#[clap(long)]
pub export_proof: Option<PathBuf>,
#[arg(long, value_parser = parse::state_version)]
pub overwrite_state_version: Option<StateVersion>,
}
#[derive(Debug, Clone, clap::Parser)]
pub struct TryRuntimeCmd {
#[clap(flatten)]
pub shared: SharedParams,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Clone, clap::Args)]
pub struct LiveState {
#[arg(
short,
long,
value_parser = parse::url,
)]
uri: String,
#[arg(
short,
long,
value_parser = parse::hash,
)]
at: Option<String>,
#[arg(short, long, num_args = 1..)]
pallet: Vec<String>,
#[arg(long)]
child_tree: bool,
}
#[derive(Debug, Clone, clap::Subcommand)]
pub enum State {
Snap {
#[arg(short, long)]
snapshot_path: PathBuf,
},
Live(LiveState),
}
impl State {
pub(crate) async fn into_ext<Block: BlockT + DeserializeOwned, HostFns: HostFunctions>(
&self,
shared: &SharedParams,
executor: &WasmExecutor<HostFns>,
state_snapshot: Option<SnapshotConfig>,
try_runtime_check: bool,
) -> sc_cli::Result<RemoteExternalities<Block>>
where
Block::Header: DeserializeOwned,
<Block::Hash as FromStr>::Err: Debug,
{
let builder = match self {
State::Snap { snapshot_path } =>
Builder::<Block>::new().mode(Mode::Offline(OfflineConfig {
state_snapshot: SnapshotConfig::new(snapshot_path),
})),
State::Live(LiveState { pallet, uri, at, child_tree }) => {
let at = match at {
Some(at_str) => Some(hash_of::<Block>(at_str)?),
None => None,
};
Builder::<Block>::new().mode(Mode::Online(OnlineConfig {
at,
transport: uri.to_owned().into(),
state_snapshot,
pallets: pallet.clone(),
child_trie: *child_tree,
hashed_keys: vec![
well_known_keys::CODE.to_vec(),
[twox_128(b"System"), twox_128(b"LastRuntimeUpgrade")].concat(),
[twox_128(b"System"), twox_128(b"Number")].concat(),
],
hashed_prefixes: vec![],
}))
},
};
let builder = if let Some(state_version) = shared.overwrite_state_version {
log::warn!(
target: LOG_TARGET,
"overwriting state version to {:?}, you better know what you are doing.",
state_version
);
builder.overwrite_state_version(state_version)
} else {
builder
};
let maybe_code_to_overwrite = match shared.runtime {
Runtime::Path(ref path) => Some(std::fs::read(path).map_err(|e| {
format!("error while reading runtime file from {:?}: {:?}", path, e)
})?),
Runtime::Existing => None,
};
let mut ext = builder.build().await?;
if let Some(new_code) = maybe_code_to_overwrite {
let original_code = ext
.execute_with(|| sp_io::storage::get(well_known_keys::CODE))
.expect("':CODE:' is always downloaded in try-runtime-cli; qed");
ext.insert(well_known_keys::CODE.to_vec(), new_code.clone());
let old_version = <RuntimeVersion as Decode>::decode(
&mut &*executor.read_runtime_version(&original_code, &mut ext.ext()).unwrap(),
)
.unwrap();
log::info!(
target: LOG_TARGET,
"original spec: {:?}-{:?}, code hash: {:?}",
old_version.spec_name,
old_version.spec_version,
HexDisplay::from(BlakeTwo256::hash(&original_code).as_fixed_bytes()),
);
let new_version = <RuntimeVersion as Decode>::decode(
&mut &*executor.read_runtime_version(&new_code, &mut ext.ext()).unwrap(),
)
.unwrap();
log::info!(
target: LOG_TARGET,
"new spec: {:?}-{:?}, code hash: {:?}",
new_version.spec_name,
new_version.spec_version,
HexDisplay::from(BlakeTwo256::hash(&new_code).as_fixed_bytes())
);
if new_version.spec_name != old_version.spec_name {
return Err("Spec names must match.".into())
}
}
if try_runtime_check {
if !ensure_try_runtime::<Block, HostFns>(&executor, &mut ext) {
return Err("given runtime is NOT compiled with try-runtime feature!".into())
}
}
Ok(ext)
}
}
pub const DEPRECATION_NOTICE: &str = "Substrate's `try-runtime` subcommand has been migrated to a standalone CLI (https://github.com/paritytech/try-runtime-cli). It is no longer being maintained here and will be removed entirely some time after January 2024. Please remove this subcommand from your runtime and use the standalone CLI.";
impl TryRuntimeCmd {
#[deprecated(
note = "Substrate's `try-runtime` subcommand has been migrated to a standalone CLI (https://github.com/paritytech/try-runtime-cli). It is no longer being maintained here and will be removed entirely some time after January 2024. Please remove this subcommand from your runtime and use the standalone CLI."
)]
pub async fn run<Block, HostFns, BBIP>(
&self,
block_building_info_provider: Option<BBIP>,
) -> sc_cli::Result<()>
where
Block: BlockT<Hash = H256> + DeserializeOwned,
Block::Header: DeserializeOwned,
<Block::Hash as FromStr>::Err: Debug,
<NumberFor<Block> as FromStr>::Err: Debug,
<NumberFor<Block> as TryInto<u64>>::Error: Debug,
NumberFor<Block>: FromStr,
HostFns: HostFunctions,
BBIP: BlockBuildingInfoProvider<Block, Option<(InherentData, Digest)>>,
{
match &self.command {
Command::OnRuntimeUpgrade(ref cmd) =>
commands::on_runtime_upgrade::on_runtime_upgrade::<Block, HostFns>(
self.shared.clone(),
cmd.clone(),
)
.await,
Command::OffchainWorker(cmd) =>
commands::offchain_worker::offchain_worker::<Block, HostFns>(
self.shared.clone(),
cmd.clone(),
)
.await,
Command::ExecuteBlock(cmd) =>
commands::execute_block::execute_block::<Block, HostFns>(
self.shared.clone(),
cmd.clone(),
)
.await,
Command::FollowChain(cmd) =>
commands::follow_chain::follow_chain::<Block, HostFns>(
self.shared.clone(),
cmd.clone(),
)
.await,
Command::FastForward(cmd) =>
commands::fast_forward::fast_forward::<Block, HostFns, BBIP>(
self.shared.clone(),
cmd.clone(),
block_building_info_provider,
)
.await,
Command::CreateSnapshot(cmd) =>
commands::create_snapshot::create_snapshot::<Block, HostFns>(
self.shared.clone(),
cmd.clone(),
)
.await,
}
}
}
impl CliConfiguration for TryRuntimeCmd {
fn shared_params(&self) -> &sc_cli::SharedParams {
&self.shared.shared_params
}
fn chain_id(&self, _is_dev: bool) -> sc_cli::Result<String> {
Ok(match self.shared.shared_params.chain {
Some(ref chain) => chain.clone(),
None => "dev".into(),
})
}
}
pub(crate) fn hash_of<Block: BlockT>(hash_str: &str) -> sc_cli::Result<Block::Hash>
where
<Block::Hash as FromStr>::Err: Debug,
{
hash_str
.parse::<<Block as BlockT>::Hash>()
.map_err(|e| format!("Could not parse block hash: {:?}", e).into())
}
pub(crate) fn full_extensions<H: HostFunctions>(wasm_executor: WasmExecutor<H>) -> Extensions {
let mut extensions = Extensions::default();
let (offchain, _offchain_state) = TestOffchainExt::new();
let (pool, _pool_state) = TestTransactionPoolExt::new();
let keystore = MemoryKeystore::new();
extensions.register(OffchainDbExt::new(offchain.clone()));
extensions.register(OffchainWorkerExt::new(offchain));
extensions.register(KeystoreExt::new(keystore));
extensions.register(TransactionPoolExt::new(pool));
extensions.register(ReadRuntimeVersionExt::new(wasm_executor));
extensions
}
pub(crate) fn build_executor<H: HostFunctions>(shared: &SharedParams) -> WasmExecutor<H> {
let heap_pages = shared
.heap_pages
.map_or(DEFAULT_HEAP_ALLOC_STRATEGY, |p| HeapAllocStrategy::Static { extra_pages: p as _ });
WasmExecutor::builder()
.with_execution_method(execution_method_from_cli(
shared.wasm_method,
shared.wasmtime_instantiation_strategy,
))
.with_onchain_heap_alloc_strategy(heap_pages)
.with_offchain_heap_alloc_strategy(heap_pages)
.build()
}
fn ensure_try_runtime<Block: BlockT, HostFns: HostFunctions>(
executor: &WasmExecutor<HostFns>,
ext: &mut TestExternalities<HashingFor<Block>>,
) -> bool {
use sp_api::RuntimeApiInfo;
let final_code = ext
.execute_with(|| sp_io::storage::get(well_known_keys::CODE))
.expect("':CODE:' is always downloaded in try-runtime-cli; qed");
let final_version = <RuntimeVersion as Decode>::decode(
&mut &*executor.read_runtime_version(&final_code, &mut ext.ext()).unwrap(),
)
.unwrap();
final_version
.api_version(&<dyn frame_try_runtime::TryRuntime<Block>>::ID)
.is_some()
}
pub(crate) fn state_machine_call<Block: BlockT, HostFns: HostFunctions>(
ext: &TestExternalities<HashingFor<Block>>,
executor: &WasmExecutor<HostFns>,
method: &'static str,
data: &[u8],
mut extensions: Extensions,
) -> sc_cli::Result<(OverlayedChanges<HashingFor<Block>>, Vec<u8>)> {
let mut changes = Default::default();
let encoded_results = StateMachine::new(
&ext.backend,
&mut changes,
executor,
method,
data,
&mut extensions,
&sp_state_machine::backend::BackendRuntimeCode::new(&ext.backend).runtime_code()?,
CallContext::Offchain,
)
.execute()
.map_err(|e| format!("failed to execute '{}': {}", method, e))
.map_err::<sc_cli::Error, _>(Into::into)?;
Ok((changes, encoded_results))
}
pub(crate) fn state_machine_call_with_proof<Block: BlockT, HostFns: HostFunctions>(
ext: &TestExternalities<HashingFor<Block>>,
executor: &WasmExecutor<HostFns>,
method: &'static str,
data: &[u8],
mut extensions: Extensions,
maybe_export_proof: Option<PathBuf>,
) -> sc_cli::Result<(OverlayedChanges<HashingFor<Block>>, Vec<u8>)> {
use parity_scale_codec::Encode;
let mut changes = Default::default();
let backend = ext.backend.clone();
let runtime_code_backend = sp_state_machine::backend::BackendRuntimeCode::new(&backend);
let proving_backend =
TrieBackendBuilder::wrap(&backend).with_recorder(Default::default()).build();
let runtime_code = runtime_code_backend.runtime_code()?;
let pre_root = *backend.root();
let encoded_results = StateMachine::new(
&proving_backend,
&mut changes,
executor,
method,
data,
&mut extensions,
&runtime_code,
CallContext::Offchain,
)
.execute()
.map_err(|e| format!("failed to execute {}: {}", method, e))
.map_err::<sc_cli::Error, _>(Into::into)?;
let proof = proving_backend
.extract_proof()
.expect("A recorder was set and thus, a storage proof can be extracted; qed");
if let Some(path) = maybe_export_proof {
let mut file = std::fs::File::create(&path).map_err(|e| {
log::error!(
target: LOG_TARGET,
"Failed to create file {}: {:?}",
path.to_string_lossy(),
e
);
e
})?;
log::info!(target: LOG_TARGET, "Writing storage proof to {}", path.to_string_lossy());
use std::io::Write as _;
file.write_all(storage_proof_to_raw_json(&proof).as_bytes()).map_err(|e| {
log::error!(
target: LOG_TARGET,
"Failed to write storage proof to {}: {:?}",
path.to_string_lossy(),
e
);
e
})?;
}
let proof_size = proof.encoded_size();
let compact_proof = proof
.clone()
.into_compact_proof::<HashingFor<Block>>(pre_root)
.map_err(|e| {
log::error!(target: LOG_TARGET, "failed to generate compact proof {}: {:?}", method, e);
e
})
.unwrap_or(CompactProof { encoded_nodes: Default::default() });
let compact_proof_size = compact_proof.encoded_size();
let compressed_proof = zstd::stream::encode_all(&compact_proof.encode()[..], 0)
.map_err(|e| {
log::error!(
target: LOG_TARGET,
"failed to generate compressed proof {}: {:?}",
method,
e
);
e
})
.unwrap_or_default();
let proof_nodes = proof.into_nodes();
let humanize = |s| {
if s < 1024 * 1024 {
format!("{:.2} KB ({} bytes)", s as f64 / 1024f64, s)
} else {
format!(
"{:.2} MB ({} KB) ({} bytes)",
s as f64 / (1024f64 * 1024f64),
s as f64 / 1024f64,
s
)
}
};
log::debug!(
target: LOG_TARGET,
"proof: 0x{}... / {} nodes",
HexDisplay::from(&proof_nodes.iter().flatten().cloned().take(10).collect::<Vec<_>>()),
proof_nodes.len()
);
log::debug!(target: LOG_TARGET, "proof size: {}", humanize(proof_size));
log::debug!(target: LOG_TARGET, "compact proof size: {}", humanize(compact_proof_size),);
log::debug!(
target: LOG_TARGET,
"zstd-compressed compact proof {}",
humanize(compressed_proof.len()),
);
log::debug!(target: LOG_TARGET, "{} executed without errors.", method);
Ok((changes, encoded_results))
}
pub(crate) fn rpc_err_handler(error: impl Debug) -> &'static str {
log::error!(target: LOG_TARGET, "rpc error: {:?}", error);
"rpc error."
}
fn storage_proof_to_raw_json(storage_proof: &sp_state_machine::StorageProof) -> String {
serde_json::Value::Object(
storage_proof
.to_memory_db::<sp_runtime::traits::BlakeTwo256>()
.drain()
.iter()
.map(|(key, (value, _n))| {
(
format!("0x{}", hex::encode(key.as_bytes())),
serde_json::Value::String(format!("0x{}", hex::encode(value))),
)
})
.collect(),
)
.to_string()
}