Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions cli/tests/execute_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
}
123 changes: 101 additions & 22 deletions core/src/commands/execute_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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<u64>,

/// Block number to stop execution at.
#[arg(long, requires = "from")]
pub to: Option<u64>,

/// 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
Expand Down Expand Up @@ -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::<Vec<_>>();

let hash_list = ChainApi::<(), Block::Hash, Block::Header, SignedBlock<Block>>::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<Block>>::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::<Block, HostFns>(&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<Block>>::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::<Block, HostFns>(&shared, &command, &executor, &rpc, live_state).await
}
}

// Perform block execution on live state
pub async fn execute_block<Block, HostFns>(
shared: &SharedParams,
command: &Command,
executor: &WasmExecutor<HostFns>,
rpc: &WsClient,
live_state: LiveState,
) -> sc_cli::Result<()>
where
Block: BlockT + serde::de::DeserializeOwned,
<Block::Hash as FromStr>::Err: Debug,
Block::Hash: serde::de::DeserializeOwned,
Block::Header: serde::de::DeserializeOwned,
<NumberFor<Block> as TryInto<u64>>::Error: Debug,
HostFns: HostFunctions,
{
// The block we want to *execute* at is the block passed by the user
let execute_at = live_state.at::<Block>()?;

Expand All @@ -137,12 +216,12 @@ where
try_runtime_feature_enabled: true,
};
let ext = State::Live(prev_block_live_state)
.to_ext::<Block, HostFns>(&shared, &executor, None, runtime_checks)
.to_ext::<Block, HostFns>(shared, executor, None, runtime_checks)
.await?;

// Execute the desired block on top of it
let block =
ChainApi::<(), Block::Hash, Block::Header, SignedBlock<Block>>::block(&rpc, execute_at)
ChainApi::<(), Block::Hash, Block::Header, SignedBlock<Block>>::block(rpc, execute_at)
.await
.map_err(rpc_err_handler)?
.expect("header exists, block should also exist; qed")
Expand All @@ -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::<Block, HostFns>(
&ext,
&mut Default::default(),
&executor,
executor,
"TryRuntime_execute_block",
&payload,
full_extensions(executor.clone()),
shared.export_proof,
shared.export_proof.clone(),
)?;

Ok(())
Expand Down
Loading