use std::{fmt::Debug, path::PathBuf, str::FromStr};
use frame_remote_externalities::{
Builder, Mode, OfflineConfig, OnlineConfig, RemoteExternalities, SnapshotConfig,
};
use parity_scale_codec::Decode;
use sc_cli::{execution_method_from_cli, RuntimeVersion};
use sc_executor::{
sp_wasm_interface::HostFunctions, HeapAllocStrategy, WasmExecutor, DEFAULT_HEAP_ALLOC_STRATEGY,
};
use sp_api::{CallContext, StorageProof};
use sp_core::{
hexdisplay::HexDisplay, storage::well_known_keys, traits::ReadRuntimeVersion, twox_128, Hasher,
};
use sp_externalities::Extensions;
use sp_runtime::{
traits::{BlakeTwo256, Block as BlockT, HashingFor, Header as HeaderT},
DeserializeOwned,
};
use sp_state_machine::{OverlayedChanges, StateMachine, TestExternalities, TrieBackendBuilder};
use substrate_rpc_client::{ws_client, ChainApi};
use crate::{
common::{
parse,
shared_parameters::{Runtime, SharedParams},
},
hash_of, rpc_err_handler, LOG_TARGET,
};
#[derive(Debug, Clone, clap::Args)]
pub struct LiveState {
#[arg(
short,
long,
value_parser = parse::url,
)]
pub uri: String,
#[arg(
short,
long,
value_parser = parse::hash,
)]
pub at: Option<String>,
#[arg(short, long, num_args = 1..)]
pub pallet: Vec<String>,
#[arg(long = "prefix", value_parser = parse::hash, num_args = 1..)]
pub hashed_prefixes: Vec<String>,
#[arg(long)]
pub child_tree: bool,
}
impl LiveState {
pub fn at<Block: BlockT>(&self) -> sc_cli::Result<Option<<Block>::Hash>>
where
<Block::Hash as FromStr>::Err: Debug,
{
self.at
.clone()
.map(|s| hash_of::<Block>(s.as_str()))
.transpose()
}
pub async fn to_prev_block_live_state<Block: BlockT>(self) -> sc_cli::Result<LiveState>
where
<Block::Hash as FromStr>::Err: Debug,
{
let at = self.at::<Block>()?;
let rpc = ws_client(&self.uri).await?;
let previous_hash = ChainApi::<(), Block::Hash, Block::Header, ()>::header(&rpc, at)
.await
.map_err(rpc_err_handler)
.and_then(|maybe_header| {
maybe_header
.ok_or("header_not_found")
.map(|h| *h.parent_hash())
})?;
Ok(LiveState {
at: Some(hex::encode(previous_hash)),
..self
})
}
}
#[derive(Debug, Clone, clap::Subcommand)]
pub enum State {
Snap {
#[clap(short = 'p', long = "path", alias = "snapshot-path")]
path: Option<PathBuf>,
},
Live(LiveState),
}
#[derive(Debug, Clone, Copy)]
pub struct RuntimeChecks {
pub name_matches: bool,
pub version_increases: bool,
pub try_runtime_feature_enabled: bool,
}
impl State {
pub async fn to_ext<Block: BlockT + DeserializeOwned, HostFns: HostFunctions>(
&self,
shared: &SharedParams,
executor: &WasmExecutor<HostFns>,
state_snapshot: Option<SnapshotConfig>,
runtime_checks: RuntimeChecks,
) -> sc_cli::Result<RemoteExternalities<Block>>
where
Block::Header: DeserializeOwned,
<Block::Hash as FromStr>::Err: Debug,
{
let builder = match self {
State::Snap { path } => {
let path = path
.as_ref()
.ok_or_else(|| "no snapshot path provided".to_string())?;
Builder::<Block>::new().mode(Mode::Offline(OfflineConfig {
state_snapshot: SnapshotConfig::new(path),
}))
}
State::Live(LiveState {
pallet,
uri,
at,
child_tree,
hashed_prefixes,
}) => {
let at = match at {
Some(at_str) => Some(hash_of::<Block>(at_str)?),
None => None,
};
let hashed_prefixes = hashed_prefixes
.iter()
.map(|p_str| {
hex::decode(p_str).map_err(|e| {
format!(
"Error decoding `hashed_prefixes` hex string entry '{:?}' to bytes: {:?}",
p_str, e
)
})
})
.collect::<Result<Vec<_>, _>>()?;
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,
}))
}
};
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();
let old_code_hash =
HexDisplay::from(BlakeTwo256::hash(&original_code).as_fixed_bytes()).to_string();
log::info!(
target: LOG_TARGET,
"Original runtime [Name: {:?}] [Version: {:?}] [Code hash: 0x{}...{}]",
old_version.spec_name,
old_version.spec_version,
&old_code_hash[0..4],
&old_code_hash[old_code_hash.len() - 4..],
);
log::debug!(
target: LOG_TARGET,
"Original runtime full code hash: 0x{:?}",
old_code_hash,
);
let new_version = <RuntimeVersion as Decode>::decode(
&mut &*executor
.read_runtime_version(&new_code, &mut ext.ext())
.unwrap(),
)
.unwrap();
let new_code_hash =
HexDisplay::from(BlakeTwo256::hash(&new_code).as_fixed_bytes()).to_string();
log::info!(
target: LOG_TARGET,
"New runtime [Name: {:?}] [Version: {:?}] [Code hash: 0x{}...{}]",
new_version.spec_name,
new_version.spec_version,
&new_code_hash[0..4],
&new_code_hash[new_code_hash.len() - 4..],
);
log::debug!(
target: LOG_TARGET,
"New runtime code hash: 0x{:?}",
new_code_hash
);
if runtime_checks.name_matches && new_version.spec_name != old_version.spec_name {
return Err(
"Spec names must match. Use `--disable-spec-name-check` to disable this check."
.into(),
);
}
if runtime_checks.version_increases
&& new_version.spec_version <= old_version.spec_version
{
return Err(format!("New runtime spec version must be greater than the on-chain runtime spec version: {} <= {}. Use `--disable-spec-version-check` to disable this check.", new_version.spec_version, old_version.spec_version).into());
}
}
if runtime_checks.try_runtime_feature_enabled
&& !ensure_try_runtime::<Block, HostFns>(executor, &mut ext)
{
return Err("Given runtime is not compiled with the try-runtime feature.".into());
}
Ok(ext)
}
}
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)
.with_allow_missing_host_functions(true)
.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_result = 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_result))
}
pub(crate) fn state_machine_call_with_proof<Block: BlockT, HostFns: HostFunctions>(
ext: &TestExternalities<HashingFor<Block>>,
storage_overlay: &mut OverlayedChanges<HashingFor<Block>>,
executor: &WasmExecutor<HostFns>,
method: &'static str,
data: &[u8],
mut extensions: Extensions,
maybe_export_proof: Option<PathBuf>,
) -> sc_cli::Result<(StorageProof, Vec<u8>)> {
let runtime_code_backend = sp_state_machine::backend::BackendRuntimeCode::new(&ext.backend);
let proving_backend = TrieBackendBuilder::wrap(&ext.backend)
.with_recorder(Default::default())
.build();
let runtime_code = runtime_code_backend.runtime_code()?;
let encoded_result = StateMachine::new(
&proving_backend,
storage_overlay,
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
})?;
}
Ok((proof, encoded_result))
}
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()
}