Skip to content

Commit 98b061f

Browse files
authored
Feat: Execute a range of past blocks (#140)
Closes #11
1 parent ab92218 commit 98b061f

2 files changed

Lines changed: 159 additions & 24 deletions

File tree

‎cli/tests/execute_block.rs‎

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ use regex::Regex;
2525
use substrate_cli_test_utils as common;
2626
use tokio::process::Command;
2727

28+
/*
29+
* Test that `execute-block` works as expected.
30+
* It covers three scenarios:
31+
* 1. Passing --at to execute a specific block.
32+
* 2. Not passing --at to execute the latest block.
33+
* 3. Passing --from and --to to execute an inclusive range of blocks.
34+
*/
2835
#[tokio::test]
2936
async fn execute_block_works() {
3037
let port = 45789;
@@ -45,7 +52,7 @@ async fn execute_block_works() {
4552
// Wait some time to ensure the node is warmed up.
4653
std::thread::sleep(Duration::from_secs(90));
4754

48-
// Test passing --at
55+
// 1. Test passing --at to execute a specific block.
4956
common::run_with_timeout(Duration::from_secs(60), async move {
5057
let ws_url = format!("ws://localhost:{}", port);
5158

@@ -87,7 +94,7 @@ async fn execute_block_works() {
8794
})
8895
.await;
8996

90-
// Test not passing --at
97+
// 2. Test not passing --at should execute the latest block.
9198
common::run_with_timeout(Duration::from_secs(60), async move {
9299
let ws_url = format!("ws://localhost:{}", port);
93100

@@ -121,5 +128,54 @@ async fn execute_block_works() {
121128
.status
122129
.success());
123130
})
131+
.await;
132+
133+
// 3. Test passing --from and --to to execute a range of blocks (inclusive).
134+
common::run_with_timeout(Duration::from_secs(120), async move {
135+
let ws_url = format!("ws://localhost:{}", port);
136+
let from = 3u64;
137+
let to = 5u64;
138+
139+
fn execute_block_range(ws_url: &str, from: u64, to: u64) -> tokio::process::Child {
140+
Command::new(cargo_bin("try-runtime"))
141+
.stdout(std::process::Stdio::piped())
142+
.stderr(std::process::Stdio::piped())
143+
.arg("--runtime=existing")
144+
.args(["execute-block"])
145+
.args([format!("--from={}", from), format!("--to={}", to)])
146+
.args(["live", format!("--uri={}", ws_url).as_str()])
147+
.kill_on_drop(true)
148+
.spawn()
149+
.unwrap()
150+
}
151+
152+
let block_execution = execute_block_range(&ws_url, from, to);
153+
154+
let output = block_execution.wait_with_output().await.unwrap();
155+
let stderr = String::from_utf8_lossy(&output.stderr);
156+
157+
// Assert that every block from `from` to `to` was executed.
158+
for block_number in from..=to {
159+
let expected_output = format!(r#".*Block #{block_number} successfully executed"#);
160+
let re = Regex::new(&expected_output).unwrap();
161+
assert!(
162+
re.is_match(&stderr),
163+
"expected block {block_number} to be executed"
164+
);
165+
}
166+
167+
// Assert that the blocks immediately outside the range were not executed.
168+
for block_number in [from - 1, to + 1] {
169+
let expected_output = format!(r#".*Block #{block_number} successfully executed"#);
170+
let re = Regex::new(&expected_output).unwrap();
171+
assert!(
172+
!re.is_match(&stderr),
173+
"block {block_number} should not have been executed"
174+
);
175+
}
176+
177+
// Assert that the block-execution exited successfully.
178+
assert!(output.status.success());
179+
})
124180
.await
125181
}

‎core/src/commands/execute_block.rs‎

Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@
1818
use std::{fmt::Debug, str::FromStr};
1919

2020
use parity_scale_codec::Encode;
21-
use sc_executor::sp_wasm_interface::HostFunctions;
21+
use sc_executor::{sp_wasm_interface::HostFunctions, WasmExecutor};
22+
use sp_rpc::{list::ListOrValue, number::NumberOrHex};
2223
use sp_runtime::{
2324
generic::SignedBlock,
2425
traits::{Block as BlockT, Header as HeaderT, NumberFor},
2526
};
26-
use substrate_rpc_client::{ws_client, ChainApi};
27+
use substrate_rpc_client::{ws_client, ChainApi, WsClient};
2728

2829
use crate::{
2930
common::state::{
@@ -50,6 +51,14 @@ pub struct Command {
5051
#[arg(long, default_value = "all")]
5152
pub try_state: frame_try_runtime::TryStateSelect,
5253

54+
/// Block number to start execution from.
55+
#[arg(long, requires = "to")]
56+
pub from: Option<u64>,
57+
58+
/// Block number to stop execution at.
59+
#[arg(long, requires = "from")]
60+
pub to: Option<u64>,
61+
5362
/// The ws uri from which to fetch the block.
5463
///
5564
/// This will always fetch the next block of whatever `state` is referring to, because this is
@@ -98,33 +107,103 @@ where
98107
let block_ws_uri = command.block_ws_uri();
99108
let rpc = ws_client(&block_ws_uri).await?;
100109

101-
let live_state = match command.state {
102-
State::Live(live_state) => {
103-
// If no --at is provided, get the latest block to replay
104-
if live_state.at.is_some() {
105-
live_state
106-
} else {
110+
// If --from and --to is passed, they take precedence over LiveState --at.
111+
if let (Some(from), Some(to)) = (command.from, command.to) {
112+
if from > to {
113+
return Err(sc_cli::Error::Application(
114+
format!("--from ({from}) must be less than or equal to --to ({to})").into(),
115+
));
116+
}
117+
118+
let block_numbers = (from..=to).map(NumberOrHex::Number).collect::<Vec<_>>();
119+
120+
let hash_list = ChainApi::<(), Block::Hash, Block::Header, SignedBlock<Block>>::block_hash(
121+
&rpc,
122+
Some(ListOrValue::List(block_numbers)),
123+
)
124+
.await
125+
.map_err(rpc_err_handler)?;
126+
127+
if let ListOrValue::List(hashes) = hash_list {
128+
for (block_number, hash) in (from..=to).zip(hashes) {
129+
let Some(hash) = hash else {
130+
log::warn!(target: LOG_TARGET, "skipping block {block_number}, hash was None");
131+
continue;
132+
};
133+
134+
log::info!(target: LOG_TARGET, "hash found, block number: {block_number}, hash: {hash}");
135+
107136
let header =
108137
ChainApi::<(), Block::Hash, Block::Header, SignedBlock<Block>>::header(
109-
&rpc, None,
138+
&rpc,
139+
Some(hash),
110140
)
111141
.await
112142
.map_err(rpc_err_handler)?
113-
.expect("header exists, block should also exist; qed");
114-
LiveState {
115-
uri: vec![block_ws_uri],
143+
.expect("hash exists, header should exist;");
144+
145+
let live_state = LiveState {
146+
uri: vec![block_ws_uri.clone()],
116147
at: Some(hex::encode(header.hash().encode())),
117148
pallet: Default::default(),
118149
hashed_prefixes: Default::default(),
119150
child_tree: Default::default(),
120-
}
151+
};
152+
153+
execute_block::<Block, HostFns>(&shared, &command, &executor, &rpc, live_state)
154+
.await?;
121155
}
122156
}
123-
_ => {
124-
unreachable!("execute block currently only supports Live state")
125-
}
126-
};
127157

158+
Ok(())
159+
} else {
160+
let live_state = match &command.state {
161+
State::Live(live_state) => {
162+
// If no --at is provided, get the latest block to replay
163+
if live_state.at.is_some() {
164+
live_state.clone()
165+
} else {
166+
let header =
167+
ChainApi::<(), Block::Hash, Block::Header, SignedBlock<Block>>::header(
168+
&rpc, None,
169+
)
170+
.await
171+
.map_err(rpc_err_handler)?
172+
.expect("header exists, block should also exist; qed");
173+
LiveState {
174+
uri: vec![block_ws_uri],
175+
at: Some(hex::encode(header.hash().encode())),
176+
pallet: Default::default(),
177+
hashed_prefixes: Default::default(),
178+
child_tree: Default::default(),
179+
}
180+
}
181+
}
182+
_ => {
183+
unreachable!("execute block currently only supports Live state")
184+
}
185+
};
186+
187+
execute_block::<Block, HostFns>(&shared, &command, &executor, &rpc, live_state).await
188+
}
189+
}
190+
191+
// Perform block execution on live state
192+
pub async fn execute_block<Block, HostFns>(
193+
shared: &SharedParams,
194+
command: &Command,
195+
executor: &WasmExecutor<HostFns>,
196+
rpc: &WsClient,
197+
live_state: LiveState,
198+
) -> sc_cli::Result<()>
199+
where
200+
Block: BlockT + serde::de::DeserializeOwned,
201+
<Block::Hash as FromStr>::Err: Debug,
202+
Block::Hash: serde::de::DeserializeOwned,
203+
Block::Header: serde::de::DeserializeOwned,
204+
<NumberFor<Block> as TryInto<u64>>::Error: Debug,
205+
HostFns: HostFunctions,
206+
{
128207
// The block we want to *execute* at is the block passed by the user
129208
let execute_at = live_state.at::<Block>()?;
130209

@@ -137,12 +216,12 @@ where
137216
try_runtime_feature_enabled: true,
138217
};
139218
let ext = State::Live(prev_block_live_state)
140-
.to_ext::<Block, HostFns>(&shared, &executor, None, runtime_checks)
219+
.to_ext::<Block, HostFns>(shared, executor, None, runtime_checks)
141220
.await?;
142221

143222
// Execute the desired block on top of it
144223
let block =
145-
ChainApi::<(), Block::Hash, Block::Header, SignedBlock<Block>>::block(&rpc, execute_at)
224+
ChainApi::<(), Block::Hash, Block::Header, SignedBlock<Block>>::block(rpc, execute_at)
146225
.await
147226
.map_err(rpc_err_handler)?
148227
.expect("header exists, block should also exist; qed")
@@ -161,18 +240,18 @@ where
161240
block.clone(),
162241
state_root_check,
163242
signature_check,
164-
command.try_state,
243+
command.try_state.clone(),
165244
)
166245
.encode();
167246

168247
let _ = state_machine_call_with_proof::<Block, HostFns>(
169248
&ext,
170249
&mut Default::default(),
171-
&executor,
250+
executor,
172251
"TryRuntime_execute_block",
173252
&payload,
174253
full_extensions(executor.clone()),
175-
shared.export_proof,
254+
shared.export_proof.clone(),
176255
)?;
177256

178257
Ok(())

0 commit comments

Comments
 (0)