diff --git a/cli/tests/execute_block.rs b/cli/tests/execute_block.rs index 18e8180769d..d27d2b9b29a 100644 --- a/cli/tests/execute_block.rs +++ b/cli/tests/execute_block.rs @@ -121,5 +121,45 @@ async fn execute_block_works() { .status .success()); }) + .await; + + // Test passing --from and --to to execute a range of blocks. + common::run_with_timeout(Duration::from_secs(120), async move { + let ws_url = format!("ws://localhost:{}", port); + let from = 3u64; + let to = 5u64; + + fn execute_block_range(ws_url: &str, from: u64, to: u64) -> tokio::process::Child { + Command::new(cargo_bin("try-runtime")) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .arg("--runtime=existing") + .args(["execute-block"]) + .args([format!("--from={}", from), format!("--to={}", to)]) + .args(["live", format!("--uri={}", ws_url).as_str()]) + .kill_on_drop(true) + .spawn() + .unwrap() + } + + let mut block_execution = execute_block_range(&ws_url, from, to); + + // Expect the last block in the range to be successfully executed. + let expected_output = format!(r#".*Block #{} successfully executed"#, to); + let re = Regex::new(expected_output.as_str()).unwrap(); + let matched = + common::wait_for_stream_pattern_match(block_execution.stderr.take().unwrap(), re).await; + + // Assert that all blocks in the range were executed. + assert!(matched.is_ok()); + + // Assert that the block-execution exited successfully. + assert!(block_execution + .wait_with_output() + .await + .unwrap() + .status + .success()); + }) .await } diff --git a/core/src/commands/execute_block.rs b/core/src/commands/execute_block.rs index 68125160410..f1fc789af1e 100644 --- a/core/src/commands/execute_block.rs +++ b/core/src/commands/execute_block.rs @@ -18,12 +18,13 @@ use std::{fmt::Debug, str::FromStr}; use parity_scale_codec::Encode; -use sc_executor::sp_wasm_interface::HostFunctions; +use sc_executor::{sp_wasm_interface::HostFunctions, WasmExecutor}; +use sp_rpc::{list::ListOrValue, number::NumberOrHex}; use sp_runtime::{ generic::SignedBlock, traits::{Block as BlockT, Header as HeaderT, NumberFor}, }; -use substrate_rpc_client::{ws_client, ChainApi}; +use substrate_rpc_client::{ws_client, ChainApi, WsClient}; use crate::{ common::state::{ @@ -50,6 +51,14 @@ pub struct Command { #[arg(long, default_value = "all")] pub try_state: frame_try_runtime::TryStateSelect, + /// Block number to start execution from. + #[arg(long, requires = "to")] + pub from: Option, + + /// Block number to stop execution at. + #[arg(long, requires = "from")] + pub to: Option, + /// The ws uri from which to fetch the block. /// /// This will always fetch the next block of whatever `state` is referring to, because this is @@ -98,33 +107,106 @@ where let block_ws_uri = command.block_ws_uri(); let rpc = ws_client(&block_ws_uri).await?; - let live_state = match command.state { - State::Live(live_state) => { - // If no --at is provided, get the latest block to replay - if live_state.at.is_some() { - live_state - } else { + // If --from and --to is passed, they take precedence over LiveState --at. + if let (Some(from), Some(to)) = (command.from, command.to) { + if from > to { + return Err(sc_cli::Error::Application( + format!("--from ({from}) must be less than or equal to --to ({to})").into(), + )); + } + + let block_numbers = (from..=to) + .map(|n| NumberOrHex::Number(n)) + .collect::>(); + + let hash_list = ChainApi::<(), Block::Hash, Block::Header, SignedBlock>::block_hash( + &rpc, + Some(ListOrValue::List(block_numbers)), + ) + .await + .map_err(rpc_err_handler)?; + + if let ListOrValue::List(hashes) = hash_list { + for (block_number, hash) in (from..=to).zip(hashes) { + let Some(hash) = hash else { + log::warn!(target: LOG_TARGET, "skipping block {block_number}, hash was None"); + continue; + }; + + log::info!(target: LOG_TARGET, "hash found, block number: {block_number}, hash: {hash}"); + let header = ChainApi::<(), Block::Hash, Block::Header, SignedBlock>::header( - &rpc, None, + &rpc, + Some(hash), ) .await .map_err(rpc_err_handler)? - .expect("header exists, block should also exist; qed"); - LiveState { - uri: vec![block_ws_uri], + .expect("hash exists, header should exist;"); + + let live_state = LiveState { + uri: vec![block_ws_uri.clone()], at: Some(hex::encode(header.hash().encode())), pallet: Default::default(), hashed_prefixes: Default::default(), child_tree: Default::default(), - } + }; + + let _ = + execute_block::(&shared, &command, &executor, &rpc, live_state) + .await; } } - _ => { - unreachable!("execute block currently only supports Live state") - } - }; + Ok(()) + } else { + let live_state = match &command.state { + State::Live(live_state) => { + // If no --at is provided, get the latest block to replay + if live_state.at.is_some() { + live_state.clone() + } else { + let header = + ChainApi::<(), Block::Hash, Block::Header, SignedBlock>::header( + &rpc, None, + ) + .await + .map_err(rpc_err_handler)? + .expect("header exists, block should also exist; qed"); + LiveState { + uri: vec![block_ws_uri], + at: Some(hex::encode(header.hash().encode())), + pallet: Default::default(), + hashed_prefixes: Default::default(), + child_tree: Default::default(), + } + } + } + _ => { + unreachable!("execute block currently only supports Live state") + } + }; + + execute_block::(&shared, &command, &executor, &rpc, live_state).await + } +} + +// Perform block execution on live state +pub async fn execute_block( + shared: &SharedParams, + command: &Command, + executor: &WasmExecutor, + rpc: &WsClient, + live_state: LiveState, +) -> sc_cli::Result<()> +where + Block: BlockT + serde::de::DeserializeOwned, + ::Err: Debug, + Block::Hash: serde::de::DeserializeOwned, + Block::Header: serde::de::DeserializeOwned, + as TryInto>::Error: Debug, + HostFns: HostFunctions, +{ // The block we want to *execute* at is the block passed by the user let execute_at = live_state.at::()?; @@ -142,7 +224,7 @@ where // Execute the desired block on top of it let block = - ChainApi::<(), Block::Hash, Block::Header, SignedBlock>::block(&rpc, execute_at) + ChainApi::<(), Block::Hash, Block::Header, SignedBlock>::block(rpc, execute_at) .await .map_err(rpc_err_handler)? .expect("header exists, block should also exist; qed") @@ -161,7 +243,7 @@ where block.clone(), state_root_check, signature_check, - command.try_state, + command.try_state.clone(), ) .encode(); @@ -172,7 +254,7 @@ where "TryRuntime_execute_block", &payload, full_extensions(executor.clone()), - shared.export_proof, + shared.export_proof.clone(), )?; Ok(())