diff --git a/cli/tests/execute_block.rs b/cli/tests/execute_block.rs index 18e8180769d..56999f8964b 100644 --- a/cli/tests/execute_block.rs +++ b/cli/tests/execute_block.rs @@ -25,6 +25,13 @@ use regex::Regex; use substrate_cli_test_utils as common; use tokio::process::Command; +/* + * Test that `execute-block` works as expected. + * It covers three scenarios: + * 1. Passing --at to execute a specific block. + * 2. Not passing --at to execute the latest block. + * 3. Passing --from and --to to execute an inclusive range of blocks. + */ #[tokio::test] async fn execute_block_works() { let port = 45789; @@ -45,7 +52,7 @@ async fn execute_block_works() { // Wait some time to ensure the node is warmed up. std::thread::sleep(Duration::from_secs(90)); - // Test passing --at + // 1. Test passing --at to execute a specific block. common::run_with_timeout(Duration::from_secs(60), async move { let ws_url = format!("ws://localhost:{}", port); @@ -87,7 +94,7 @@ async fn execute_block_works() { }) .await; - // Test not passing --at + // 2. Test not passing --at should execute the latest block. common::run_with_timeout(Duration::from_secs(60), async move { let ws_url = format!("ws://localhost:{}", port); @@ -121,5 +128,54 @@ async fn execute_block_works() { .status .success()); }) + .await; + + // 3. Test passing --from and --to to execute a range of blocks (inclusive). + 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 block_execution = execute_block_range(&ws_url, from, to); + + let output = block_execution.wait_with_output().await.unwrap(); + let stderr = String::from_utf8_lossy(&output.stderr); + + // Assert that every block from `from` to `to` was executed. + for block_number in from..=to { + let expected_output = format!(r#".*Block #{block_number} successfully executed"#); + let re = Regex::new(&expected_output).unwrap(); + assert!( + re.is_match(&stderr), + "expected block {block_number} to be executed" + ); + } + + // Assert that the blocks immediately outside the range were not executed. + for block_number in [from - 1, to + 1] { + let expected_output = format!(r#".*Block #{block_number} successfully executed"#); + let re = Regex::new(&expected_output).unwrap(); + assert!( + !re.is_match(&stderr), + "block {block_number} should not have been executed" + ); + } + + // Assert that the block-execution exited successfully. + assert!(output.status.success()); + }) .await } diff --git a/core/src/commands/execute_block.rs b/core/src/commands/execute_block.rs index 68125160410..5ba4216516a 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,103 @@ 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(NumberOrHex::Number).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(), - } + }; + + 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::()?; @@ -137,12 +216,12 @@ where try_runtime_feature_enabled: true, }; let ext = State::Live(prev_block_live_state) - .to_ext::(&shared, &executor, None, runtime_checks) + .to_ext::(shared, executor, None, runtime_checks) .await?; // 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,18 +240,18 @@ where block.clone(), state_root_check, signature_check, - command.try_state, + command.try_state.clone(), ) .encode(); let _ = state_machine_call_with_proof::( &ext, &mut Default::default(), - &executor, + executor, "TryRuntime_execute_block", &payload, full_extensions(executor.clone()), - shared.export_proof, + shared.export_proof.clone(), )?; Ok(())