referrerpolicy=no-referrer-when-downgrade

frame_remote_externalities/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # Remote Externalities
19//!
20//! An equivalent of `sp_io::TestExternalities` that can load its state from a remote substrate
21//! based chain, or a local state snapshot file.
22
23mod client;
24mod config;
25mod key_range;
26mod logging;
27mod parallel;
28
29pub use config::{Mode, OfflineConfig, OnlineConfig, SnapshotConfig};
30
31use client::{with_timeout, Client, ConnectionManager, RPC_TIMEOUT};
32use codec::Encode;
33use config::Snapshot;
34#[cfg(all(test, feature = "remote-test"))]
35use config::DEFAULT_WS_ENDPOINT;
36use indicatif::{ProgressBar, ProgressStyle};
37use jsonrpsee::core::params::ArrayParams;
38use log::*;
39use parallel::{run_workers, ProcessResult, RetryAction};
40use serde::de::DeserializeOwned;
41use sp_core::{
42	hexdisplay::HexDisplay,
43	storage::{
44		well_known_keys::{is_default_child_storage_key, DEFAULT_CHILD_STORAGE_KEY_PREFIX},
45		ChildInfo, ChildType, PrefixedStorageKey, StorageData, StorageKey,
46	},
47};
48use sp_runtime::{
49	traits::{Block as BlockT, HashingFor, Header as HeaderT},
50	StateVersion,
51};
52use sp_state_machine::TestExternalities;
53use std::{
54	collections::{BTreeSet, VecDeque},
55	future::Future,
56	ops::{Deref, DerefMut},
57	sync::{
58		atomic::{AtomicUsize, Ordering},
59		Arc, Mutex,
60	},
61	time::Duration,
62};
63use substrate_rpc_client::{rpc_params, BatchRequestBuilder, ChainApi, ClientT, StateApi};
64
65use crate::key_range::{initialize_work_queue, subdivide_remaining_range};
66
67type Result<T, E = &'static str> = std::result::Result<T, E>;
68
69type KeyValue = (StorageKey, StorageData);
70type TopKeyValues = Vec<KeyValue>;
71type ChildKeyValues = Vec<(ChildInfo, Vec<KeyValue>)>;
72
73const LOG_TARGET: &str = "remote-ext";
74
75/// Whether a stringified RPC error reports that the provider does not have the requested block.
76///
77/// Such a provider is lagging or pruning and should be dropped rather than retried (see the
78/// `UnknownBlock` error in `sp_blockchain`).
79fn is_unknown_block_error(error: &str) -> bool {
80	error.contains("UnknownBlock")
81}
82
83/// How to handle the worker's client after a failed RPC: drop a provider that lacks the block,
84/// otherwise reconnect and retry.
85fn retry_action(error: &str) -> RetryAction {
86	if is_unknown_block_error(error) {
87		RetryAction::Remove
88	} else {
89		RetryAction::Recreate
90	}
91}
92
93/// An externalities that acts exactly the same as [`sp_io::TestExternalities`] but has a few extra
94/// bits and pieces to it, and can be loaded remotely.
95pub struct RemoteExternalities<B: BlockT> {
96	/// The inner externalities.
97	pub inner_ext: TestExternalities<HashingFor<B>>,
98	/// The block header which we created this externalities env.
99	pub header: B::Header,
100}
101
102impl<B: BlockT> Deref for RemoteExternalities<B> {
103	type Target = TestExternalities<HashingFor<B>>;
104	fn deref(&self) -> &Self::Target {
105		&self.inner_ext
106	}
107}
108
109impl<B: BlockT> DerefMut for RemoteExternalities<B> {
110	fn deref_mut(&mut self) -> &mut Self::Target {
111		&mut self.inner_ext
112	}
113}
114
115/// Builder for [`RemoteExternalities`].
116#[derive(Clone)]
117pub struct Builder<B: BlockT> {
118	/// Custom key-pairs to be injected into the final externalities. The *hashed* keys and values
119	/// must be given.
120	hashed_key_values: Vec<KeyValue>,
121	/// The keys that will be excluded from the final externality. The *hashed* key must be given.
122	hashed_blacklist: Vec<Vec<u8>>,
123	/// Connectivity mode, online or offline.
124	mode: Mode<B::Hash>,
125	/// If provided, overwrite the state version with this. Otherwise, the state_version of the
126	/// remote node is used. All cache files also store their state version.
127	///
128	/// Overwrite only with care.
129	overwrite_state_version: Option<StateVersion>,
130	/// Connection manager for RPC clients (initialized during `init_remote_client`).
131	conn_manager: Option<ConnectionManager>,
132}
133
134impl<B: BlockT> Default for Builder<B> {
135	fn default() -> Self {
136		Self {
137			mode: Default::default(),
138			hashed_key_values: Default::default(),
139			hashed_blacklist: Default::default(),
140			overwrite_state_version: None,
141			conn_manager: None,
142		}
143	}
144}
145
146// Mode methods
147impl<B: BlockT> Builder<B> {
148	fn as_online(&self) -> &OnlineConfig<B::Hash> {
149		match &self.mode {
150			Mode::Online(config) => config,
151			Mode::OfflineOrElseOnline(_, config) => config,
152			_ => panic!("Unexpected mode: Online"),
153		}
154	}
155
156	fn as_online_mut(&mut self) -> &mut OnlineConfig<B::Hash> {
157		match &mut self.mode {
158			Mode::Online(config) => config,
159			Mode::OfflineOrElseOnline(_, config) => config,
160			_ => panic!("Unexpected mode: Online"),
161		}
162	}
163
164	fn conn_manager(&self) -> Result<&ConnectionManager> {
165		self.conn_manager.as_ref().ok_or("connection manager must be initialized; qed")
166	}
167
168	/// Whether the configured scrape covers the entire top trie.
169	///
170	/// Only a complete scrape yields a storage root that matches the block header's state root.
171	/// This is signalled by the empty prefix being queued for download (see
172	/// `init_remote_client`); partial scrapes (specific pallets/keys) never match.
173	fn is_complete_scrape(&self) -> bool {
174		self.as_online().hashed_prefixes.iter().any(|p| p.is_empty())
175	}
176}
177
178// RPC methods
179impl<B: BlockT> Builder<B>
180where
181	B::Hash: DeserializeOwned,
182	B::Header: DeserializeOwned,
183{
184	const PARALLEL_REQUESTS_PER_CLIENT: usize = 4;
185
186	async fn parallel_requests(&self) -> usize {
187		let cm = self.conn_manager().expect("connection manager must be initialized; qed");
188		cm.num_clients().await * Self::PARALLEL_REQUESTS_PER_CLIENT
189	}
190
191	/// Execute an RPC call on any available client. Tries each client until one succeeds.
192	///
193	/// Starts with a random client to distribute load across clients.
194	async fn with_any_client<T, E, F, Fut>(&self, op_name: &'static str, f: F) -> Result<T, ()>
195	where
196		F: Fn(Client) -> Fut,
197		Fut: Future<Output = std::result::Result<T, E>>,
198		E: std::fmt::Debug,
199	{
200		let conn_manager = self.conn_manager().map_err(|_| ())?;
201		let num_clients = conn_manager.num_clients().await;
202		let start_offset: usize = rand::random();
203		for j in 0..num_clients {
204			let i = (start_offset + j) % num_clients;
205			let client = conn_manager.get(i).await;
206			let result = with_timeout(f(client), RPC_TIMEOUT).await;
207			match result {
208				Ok(Ok(value)) => return Ok(value),
209				Ok(Err(e)) => {
210					debug!(target: LOG_TARGET, "Client {i}: {op_name} RPC error: {e:?}");
211				},
212				Err(()) => {
213					debug!(target: LOG_TARGET, "Client {i}: {op_name} timeout");
214				},
215			}
216		}
217		Err(())
218	}
219
220	/// Get a single storage value. Tries each client until one succeeds.
221	async fn rpc_get_storage(
222		&self,
223		key: StorageKey,
224		maybe_at: Option<B::Hash>,
225	) -> Result<Option<StorageData>> {
226		trace!(target: LOG_TARGET, "rpc: get_storage");
227		self.with_any_client("get_storage", move |client| {
228			let key = key.clone();
229			async move { client.storage(key, maybe_at).await }
230		})
231		.await
232		.map_err(|_| "rpc get_storage failed on all clients")
233	}
234
235	/// Fetch the state version from the runtime. Tries each client until one succeeds.
236	async fn fetch_state_version(&self) -> Result<StateVersion> {
237		let conn_manager = self.conn_manager()?;
238
239		let num_clients = conn_manager.num_clients().await;
240		for i in 0..num_clients {
241			let client = conn_manager.get(i).await;
242			let result = with_timeout(
243				StateApi::<B::Hash>::runtime_version(client.ws_client.as_ref(), None),
244				RPC_TIMEOUT,
245			)
246			.await;
247
248			match result {
249				Ok(Ok(version)) => return Ok(version.state_version()),
250				Ok(Err(e)) => {
251					debug!(target: LOG_TARGET, "Client {i}: runtime_version RPC error: {e:?}");
252				},
253				Err(()) => {
254					debug!(target: LOG_TARGET, "Client {i}: runtime_version timeout");
255				},
256			}
257		}
258
259		Err("rpc runtime_version failed on all clients")
260	}
261
262	/// Get the latest finalized head. Tries each client until one succeeds.
263	async fn rpc_get_head(&self) -> Result<B::Hash> {
264		trace!(target: LOG_TARGET, "rpc: finalized_head");
265		self.with_any_client("finalized_head", |client| async move {
266			ChainApi::<(), _, B::Header, ()>::finalized_head(&*client).await
267		})
268		.await
269		.map_err(|_| "rpc finalized_head failed on all clients")
270	}
271
272	/// Get keys with `prefix` at `block` using parallel workers.
273	async fn rpc_get_keys_parallel(
274		&self,
275		prefix: &StorageKey,
276		block: B::Hash,
277		parallel: usize,
278	) -> Result<Vec<StorageKey>> {
279		let work_queue = initialize_work_queue(&[prefix.clone()]);
280		let initial_ranges = work_queue.lock().unwrap().len();
281		info!(target: LOG_TARGET, "๐Ÿ”ง Initialized work queue with {initial_ranges} ranges");
282
283		let conn_manager = self.conn_manager()?;
284		info!(target: LOG_TARGET, "๐ŸŒ Using {} RPC provider(s)", conn_manager.num_clients().await);
285		info!(target: LOG_TARGET, "๐Ÿš€ Spawning {parallel} parallel workers for key fetching");
286
287		let all_keys: Arc<Mutex<BTreeSet<StorageKey>>> = Arc::new(Mutex::new(BTreeSet::new()));
288		let last_logged_milestone = Arc::new(AtomicUsize::new(0));
289		let initial_work = work_queue.lock().unwrap().drain(..).collect();
290		let all_keys_for_result = all_keys.clone();
291
292		run_workers(initial_work, conn_manager, parallel, move |worker_index, range, client| {
293			let all_keys = all_keys.clone();
294			let last_logged_milestone = last_logged_milestone.clone();
295
296			async move {
297				trace!(
298					target: LOG_TARGET,
299					"Worker {worker_index}: fetching keys starting at {:?} (page_size: {})",
300					HexDisplay::from(&range.start_key.0),
301					range.page_size
302				);
303
304				let rpc_result = with_timeout(
305					client.storage_keys_paged(
306						Some(range.prefix.clone()),
307						range.page_size,
308						Some(range.start_key.clone()),
309						Some(block),
310					),
311					RPC_TIMEOUT,
312				)
313				.await;
314
315				let page = match rpc_result {
316					Ok(Ok(p)) => p,
317					Ok(Err(e)) => {
318						debug!(target: LOG_TARGET, "Worker {worker_index}: RPC error: {e:?}");
319						return ProcessResult::Retry {
320							work: range.with_halved_page_size(),
321							sleep_duration: Duration::from_secs(15),
322							action: retry_action(&format!("{e:?}")),
323						};
324					},
325					Err(()) => {
326						debug!(target: LOG_TARGET, "Worker {worker_index}: timeout");
327						return ProcessResult::Retry {
328							work: range.with_halved_page_size(),
329							sleep_duration: Duration::from_secs(5),
330							action: RetryAction::Recreate,
331						};
332					},
333				};
334
335				// Filter keys and determine if this was a full batch
336				let (page, is_full_batch) = range.filter_keys(page);
337				let last_two_keys = if page.len() >= 2 {
338					Some((page[page.len() - 2].clone(), page[page.len() - 1].clone()))
339				} else {
340					None
341				};
342
343				let total_keys = {
344					let mut keys = all_keys.lock().unwrap();
345					keys.extend(page);
346					keys.len()
347				};
348
349				// Log progress every 10,000 keys
350				const LOG_INTERVAL: usize = 10_000;
351				let current_milestone = (total_keys / LOG_INTERVAL) * LOG_INTERVAL;
352				let last_milestone = last_logged_milestone.load(Ordering::Relaxed);
353				if current_milestone > last_milestone && current_milestone > 0 {
354					if last_logged_milestone
355						.compare_exchange(
356							last_milestone,
357							current_milestone,
358							Ordering::SeqCst,
359							Ordering::Relaxed,
360						)
361						.is_ok()
362					{
363						info!(target: LOG_TARGET, "๐Ÿ“Š Scraped {total_keys} keys so far...");
364					}
365				}
366
367				// Subdivide remaining range if this was a full batch
368				let new_work = if is_full_batch {
369					if let Some((second_last, last)) = last_two_keys {
370						subdivide_remaining_range(
371							&second_last,
372							&last,
373							range.end_key.as_ref(),
374							&range.prefix,
375						)
376					} else {
377						vec![]
378					}
379				} else {
380					vec![]
381				};
382
383				ProcessResult::Success { new_work }
384			}
385		})
386		.await;
387
388		let keys: Vec<_> = all_keys_for_result.lock().unwrap().iter().cloned().collect();
389		info!(target: LOG_TARGET, "๐ŸŽ‰ Parallel key fetching complete: {} unique keys", keys.len());
390
391		Ok(keys)
392	}
393
394	/// Fetches storage data from a node using a dynamic batch size.
395	///
396	/// This function adjusts the batch size on the fly to help prevent overwhelming the node with
397	/// large batch requests, and stay within request size limits enforced by the node.
398	///
399	/// # Arguments
400	///
401	/// * `client` - An `Arc` wrapped `HttpClient` used for making the requests.
402	/// * `payloads` - A vector of tuples containing a JSONRPC method name and `ArrayParams`
403	///
404	/// # Returns
405	///
406	/// Returns a `Result` with a vector of `Option<StorageData>`, where each element corresponds to
407	/// the storage data for the given method and parameters. The result will be an `Err` with a
408	/// `String` error message if the request fails.
409	///
410	/// # Errors
411	///
412	/// This function will return an error if:
413	/// * The batch request fails and the batch size is less than 2.
414	/// * There are invalid batch params.
415	/// * There is an error in the batch response.
416	///
417	/// # Example
418	///
419	/// ```ignore
420	/// use your_crate::{get_storage_data_dynamic_batch_size, HttpClient, ArrayParams};
421	/// use std::sync::Arc;
422	///
423	/// async fn example() {
424	///     let client = HttpClient::new();
425	///     let payloads = vec![
426	///         ("some_method".to_string(), ArrayParams::new(vec![])),
427	///         ("another_method".to_string(), ArrayParams::new(vec![])),
428	///     ];
429	///     let initial_batch_size = 10;
430	///
431	///     let storage_data = get_storage_data_dynamic_batch_size(client, payloads, batch_size).await;
432	///     match storage_data {
433	///         Ok(data) => println!("Storage data: {:?}", data),
434	///         Err(e) => eprintln!("Error fetching storage data: {}", e),
435	///     }
436	/// }
437	/// ```
438	async fn get_storage_data_dynamic_batch_size(
439		client: &Client,
440		worker_index: usize,
441		payloads: &[(String, ArrayParams)],
442		bar: &ProgressBar,
443		batch_size: usize,
444	) -> std::result::Result<Vec<Option<StorageData>>, String> {
445		let mut all_data: Vec<Option<StorageData>> = vec![];
446		let mut start_index = 0;
447		let total_payloads = payloads.len();
448
449		while start_index < total_payloads {
450			let end_index = usize::min(start_index + batch_size, total_payloads);
451			let page = &payloads[start_index..end_index];
452
453			trace!(
454				target: LOG_TARGET,
455				"Worker {worker_index}: fetching values {start_index}..{end_index} of {total_payloads}",
456			);
457
458			// Build the batch request
459			let mut batch = BatchRequestBuilder::new();
460			for (method, params) in page.iter() {
461				if batch.insert(method, params.clone()).is_err() {
462					panic!("Invalid batch method and/or params; qed");
463				}
464			}
465
466			let rpc_result = with_timeout(
467				client.ws_client.batch_request::<Option<StorageData>>(batch),
468				RPC_TIMEOUT,
469			)
470			.await;
471
472			let batch_response = match rpc_result {
473				Ok(Ok(r)) => r,
474				Ok(Err(e)) => return Err(format!("RPC error: {e:?}")),
475				Err(()) => return Err("timeout".to_string()),
476			};
477
478			let batch_response_len = batch_response.len();
479			for item in batch_response.into_iter() {
480				match item {
481					Ok(x) => all_data.push(x),
482					Err(e) => return Err(format!("batch item error: {}", e.message())),
483				}
484			}
485			bar.inc(batch_response_len as u64);
486
487			start_index = end_index;
488		}
489
490		Ok(all_data)
491	}
492
493	/// Synonym of `getPairs` that uses paged queries to first get the keys, and then
494	/// map them to values one by one.
495	///
496	/// This can work with public nodes. But, expect it to be darn slow.
497	pub(crate) async fn rpc_get_pairs(
498		&self,
499		prefix: StorageKey,
500		at: B::Hash,
501		pending_ext: &mut TestExternalities<HashingFor<B>>,
502	) -> Result<Vec<KeyValue>> {
503		let parallel = self.parallel_requests().await;
504		let keys = logging::with_elapsed_async(
505			|| async { self.rpc_get_keys_parallel(&prefix, at, parallel).await },
506			"Scraping keys...",
507			|keys| format!("Found {} keys", keys.len()),
508		)
509		.await?;
510
511		if keys.is_empty() {
512			return Ok(Default::default());
513		}
514
515		let conn_manager = self.conn_manager()?;
516
517		let payloads = keys
518			.iter()
519			.map(|key| ("state_getStorage".to_string(), rpc_params!(key, at)))
520			.collect::<Vec<_>>();
521
522		let bar = ProgressBar::new(payloads.len() as u64);
523		bar.enable_steady_tick(Duration::from_secs(1));
524		bar.set_message("Downloading key values".to_string());
525		bar.set_style(
526			ProgressStyle::with_template(
527				"[{elapsed_precise}] {msg} {per_sec} [{wide_bar}] {pos}/{len} ({eta})",
528			)
529			.unwrap()
530			.progress_chars("=>-"),
531		);
532
533		// Create batches of payloads for dynamic work distribution
534		// Each batch is: (start_index, payloads, batch_size)
535		const BATCH_SIZE: usize = 1000;
536		let batches: VecDeque<_> = payloads
537			.chunks(BATCH_SIZE)
538			.enumerate()
539			.map(|(i, chunk)| (i * BATCH_SIZE, chunk.to_vec(), BATCH_SIZE))
540			.collect();
541
542		info!(target: LOG_TARGET, "๐Ÿ”ง Initialized {} batches for value fetching", batches.len());
543		info!(target: LOG_TARGET, "๐Ÿš€ Spawning {parallel} parallel workers for value fetching");
544
545		let results: Arc<Mutex<Vec<Option<StorageData>>>> =
546			Arc::new(Mutex::new(vec![None; payloads.len()]));
547		let results_for_extraction = results.clone();
548		let bar_for_finish = bar.clone();
549
550		run_workers(
551			batches,
552			conn_manager,
553			parallel,
554			move |worker_index, (start_index, batch, batch_size), client| {
555				let results = results.clone();
556				let bar = bar.clone();
557
558				async move {
559					debug!(
560						target: LOG_TARGET,
561						"Value worker {worker_index}: Processing batch at {start_index} with {} payloads",
562						batch.len()
563					);
564
565					match Self::get_storage_data_dynamic_batch_size(
566						&client,
567						worker_index,
568						&batch,
569						&bar,
570						batch_size,
571					)
572					.await
573					{
574						Ok(batch_results) => {
575							let mut results_lock = results.lock().unwrap();
576							for (offset, result) in batch_results.into_iter().enumerate() {
577								results_lock[start_index + offset] = result;
578							}
579							ProcessResult::Success { new_work: vec![] }
580						},
581						Err(e) => {
582							debug!(target: LOG_TARGET, "Value worker {worker_index}: failed: {e:?}");
583							let new_batch_size = (batch_size / 2).max(10);
584							ProcessResult::Retry {
585								work: (start_index, batch, new_batch_size),
586								sleep_duration: Duration::from_secs(15),
587								action: retry_action(&e),
588							}
589						},
590					}
591				}
592			},
593		)
594		.await;
595
596		let storage_data = results_for_extraction.lock().unwrap().clone();
597
598		bar_for_finish.finish_with_message("โœ… Downloaded key values");
599		println!();
600
601		// Check if we got responses for all submitted requests.
602		assert_eq!(keys.len(), storage_data.len());
603
604		// Filter out None values - keys without values should NOT be inserted
605		// (inserting with empty value would change the trie structure)
606		let key_values: Vec<_> = keys
607			.iter()
608			.zip(storage_data)
609			.filter_map(|(key, maybe_value)| maybe_value.map(|data| (key.clone(), data)))
610			.collect();
611
612		logging::with_elapsed(
613			|| {
614				pending_ext.batch_insert(key_values.clone().into_iter().filter_map(|(k, v)| {
615					// Don't insert the child keys here, they need to be inserted separately with
616					// all their data in the load_child_remote function.
617					match is_default_child_storage_key(&k.0) {
618						true => None,
619						false => Some((k.0, v.0)),
620					}
621				}));
622
623				Ok(())
624			},
625			"Inserting keys into DB...",
626			|_| "Inserted keys into DB".into(),
627		)
628		.expect("must succeed; qed");
629
630		Ok(key_values)
631	}
632
633	/// Get the values corresponding to `child_keys` at the given `prefixed_top_key`.
634	pub(crate) async fn rpc_child_get_storage_paged(
635		client: &Client,
636		prefixed_top_key: &StorageKey,
637		child_keys: Vec<StorageKey>,
638		at: B::Hash,
639	) -> Result<Vec<KeyValue>, String> {
640		let payloads: Vec<_> = child_keys
641			.iter()
642			.map(|key| {
643				(
644					"childstate_getStorage".to_string(),
645					rpc_params![
646						PrefixedStorageKey::new(prefixed_top_key.as_ref().to_vec()),
647						key,
648						at
649					],
650				)
651			})
652			.collect();
653
654		let bar = ProgressBar::new(payloads.len() as u64);
655		let storage_data =
656			Self::get_storage_data_dynamic_batch_size(client, 0, &payloads, &bar, 1000).await?;
657
658		// Filter out None values
659		Ok(child_keys
660			.into_iter()
661			.zip(storage_data)
662			.filter_map(|(key, maybe_value)| maybe_value.map(|v| (key, v)))
663			.collect())
664	}
665}
666
667impl<B: BlockT> Builder<B>
668where
669	B::Hash: DeserializeOwned,
670	B::Header: DeserializeOwned,
671{
672	/// Fetch all keys and values for a single child trie.
673	async fn fetch_single_child_trie(
674		client: &Client,
675		prefixed_top_key: &StorageKey,
676		at: B::Hash,
677	) -> Result<(ChildInfo, Vec<KeyValue>), String> {
678		let top_key = PrefixedStorageKey::new(prefixed_top_key.0.clone());
679		let page_size = 1000u32;
680
681		trace!(
682			target: LOG_TARGET,
683			"Fetching child trie keys for {:?}",
684			HexDisplay::from(&prefixed_top_key.0)
685		);
686
687		// Fetch all keys for this child trie
688		let mut child_keys = Vec::new();
689		let mut start_key: Option<StorageKey> = None;
690
691		loop {
692			let rpc_result = with_timeout(
693				substrate_rpc_client::ChildStateApi::storage_keys_paged(
694					client.ws_client.as_ref(),
695					top_key.clone(),
696					Some(StorageKey(vec![])),
697					page_size,
698					start_key.clone(),
699					Some(at),
700				),
701				RPC_TIMEOUT,
702			)
703			.await;
704
705			let page = match rpc_result {
706				Ok(Ok(p)) => p,
707				Ok(Err(e)) => {
708					debug!(target: LOG_TARGET, "Child trie RPC error: {e:?}");
709					return Err(format!("rpc child_get_keys failed: {e:?}"));
710				},
711				Err(()) => {
712					debug!(target: LOG_TARGET, "Child trie RPC timeout");
713					return Err("rpc child_get_keys timeout".to_string());
714				},
715			};
716
717			let is_full_batch = page.len() == page_size as usize;
718			start_key = page.last().cloned();
719			child_keys.extend(page);
720
721			if !is_full_batch {
722				break;
723			}
724		}
725
726		// Fetch values for all keys
727		let child_kv =
728			Self::rpc_child_get_storage_paged(client, prefixed_top_key, child_keys, at).await?;
729
730		// Parse the child info
731		let un_prefixed = match ChildType::from_prefixed_key(&top_key) {
732			Some((ChildType::ParentKeyId, storage_key)) => storage_key,
733			None => return Err("invalid child key".to_string()),
734		};
735
736		Ok((ChildInfo::new_default(un_prefixed), child_kv))
737	}
738
739	/// Load all of the child keys from the remote config, given the already scraped list of top key
740	/// pairs.
741	///
742	/// `top_kv` need not be only child-bearing top keys. It should be all of the top keys that are
743	/// included thus far.
744	///
745	/// This function uses parallel workers to fetch child tries concurrently.
746	async fn load_child_remote(
747		&self,
748		top_kv: &[KeyValue],
749		pending_ext: &mut TestExternalities<HashingFor<B>>,
750	) -> Result<ChildKeyValues> {
751		let child_roots: VecDeque<StorageKey> = top_kv
752			.iter()
753			.filter(|(k, _)| is_default_child_storage_key(k.as_ref()))
754			.map(|(k, _)| k.clone())
755			.collect();
756
757		if child_roots.is_empty() {
758			info!(target: LOG_TARGET, "๐Ÿ‘ฉโ€๐Ÿ‘ฆ no child roots found to scrape");
759			return Ok(Default::default());
760		}
761
762		let total_count = child_roots.len();
763		info!(
764			target: LOG_TARGET,
765			"๐Ÿ‘ฉโ€๐Ÿ‘ฆ scraping child-tree data from {} child tries",
766			total_count,
767		);
768
769		let at = self.as_online().at_expected();
770		let conn_manager = self.conn_manager()?;
771		let parallel = self.parallel_requests().await;
772
773		let results: Arc<Mutex<Vec<(ChildInfo, Vec<KeyValue>)>>> = Arc::new(Mutex::new(Vec::new()));
774		let results_for_extraction = results.clone();
775		let completed_count = Arc::new(AtomicUsize::new(0));
776
777		run_workers(
778			child_roots,
779			conn_manager,
780			parallel,
781			move |worker_index, prefixed_top_key, client| {
782				let results = results.clone();
783				let completed_count = completed_count.clone();
784
785				async move {
786					match Self::fetch_single_child_trie(&client, &prefixed_top_key, at).await {
787						Ok((info, child_kv_inner)) => {
788							results.lock().unwrap().push((info, child_kv_inner));
789
790							let done = completed_count.fetch_add(1, Ordering::SeqCst) + 1;
791							if done.is_multiple_of(100) || done == total_count {
792								info!(
793									target: LOG_TARGET,
794									"๐Ÿ‘ฉโ€๐Ÿ‘ฆ Child tries progress: {}/{} completed",
795									done,
796									total_count
797								);
798							}
799
800							ProcessResult::Success { new_work: vec![] }
801						},
802						Err(e) => {
803							error!(target: LOG_TARGET, "Worker {worker_index}: Failed: {e:?}");
804							ProcessResult::Retry {
805								work: prefixed_top_key,
806								sleep_duration: Duration::from_secs(5),
807								action: retry_action(&e),
808							}
809						},
810					}
811				}
812			},
813		)
814		.await;
815
816		// Extract results and populate pending_ext
817		let child_kv_results = results_for_extraction.lock().unwrap().clone();
818
819		let mut child_kv = Vec::new();
820		for (info, kv_inner) in child_kv_results {
821			let key_values: Vec<(Vec<u8>, Vec<u8>)> =
822				kv_inner.iter().cloned().map(|(k, v)| (k.0, v.0)).collect();
823			for (k, v) in key_values {
824				pending_ext.insert_child(info.clone(), k, v);
825			}
826			child_kv.push((info, kv_inner));
827		}
828
829		info!(
830			target: LOG_TARGET,
831			"๐Ÿ‘ฉโ€๐Ÿ‘ฆ Completed scraping {} child tries",
832			child_kv.len()
833		);
834
835		Ok(child_kv)
836	}
837
838	/// Build `Self` from a network node denoted by `uri`.
839	///
840	/// This function concurrently populates `pending_ext`. the return value is only for writing to
841	/// cache, we can also optimize further.
842	async fn load_top_remote(
843		&self,
844		pending_ext: &mut TestExternalities<HashingFor<B>>,
845	) -> Result<TopKeyValues> {
846		let config = self.as_online();
847		let at = self
848			.as_online()
849			.at
850			.expect("online config must be initialized by this point; qed.");
851		info!(target: LOG_TARGET, "scraping key-pairs from remote at block height {at:?}");
852
853		let mut keys_and_values = Vec::new();
854		for prefix in &config.hashed_prefixes {
855			let now = std::time::Instant::now();
856			let additional_key_values =
857				self.rpc_get_pairs(StorageKey(prefix.to_vec()), at, pending_ext).await?;
858			let elapsed = now.elapsed();
859			info!(
860				target: LOG_TARGET,
861				"adding data for hashed prefix: {:?}, took {:.2}s",
862				HexDisplay::from(prefix),
863				elapsed.as_secs_f32()
864			);
865			keys_and_values.extend(additional_key_values);
866		}
867
868		for key in &config.hashed_keys {
869			let key = StorageKey(key.to_vec());
870			info!(
871				target: LOG_TARGET,
872				"adding data for hashed key: {:?}",
873				HexDisplay::from(&key)
874			);
875			match self.rpc_get_storage(key.clone(), Some(at)).await? {
876				Some(value) => {
877					pending_ext.insert(key.clone().0, value.clone().0);
878					keys_and_values.push((key, value));
879				},
880				None => {
881					warn!(
882						target: LOG_TARGET,
883						"no data found for hashed key: {:?}",
884						HexDisplay::from(&key)
885					);
886				},
887			}
888		}
889
890		Ok(keys_and_values)
891	}
892
893	/// The entry point of execution, if `mode` is online.
894	///
895	/// Initializes the remote clients and sets the `at` field if not specified.
896	async fn init_remote_client(&mut self) -> Result<()> {
897		// First, create all clients from URIs, filtering out ones that fail to connect.
898		let online_config = self.as_online();
899		let mut clients = Vec::new();
900		for uri in &online_config.transport_uris {
901			if let Some(client) = Client::new(uri.clone()).await {
902				clients.push((uri.clone(), Arc::new(tokio::sync::Mutex::new(client))));
903			}
904		}
905		self.conn_manager = Some(ConnectionManager::new(clients)?);
906
907		// Then, if `at` is not set, set it.
908		if self.as_online().at.is_none() {
909			let at = self.rpc_get_head().await?;
910			info!(
911				target: LOG_TARGET,
912				"since no at is provided, setting it to latest finalized head, {at:?}",
913			);
914			self.as_online_mut().at = Some(at);
915		}
916
917		// Then, a few transformation that we want to perform in the online config:
918		let online_config = self.as_online_mut();
919		online_config.pallets.iter().for_each(|p| {
920			online_config
921				.hashed_prefixes
922				.push(sp_crypto_hashing::twox_128(p.as_bytes()).to_vec())
923		});
924
925		if online_config.child_trie {
926			online_config.hashed_prefixes.push(DEFAULT_CHILD_STORAGE_KEY_PREFIX.to_vec());
927		}
928
929		// Finally, if by now, we have put any limitations on prefixes that we are interested in, we
930		// download everything.
931		if online_config
932			.hashed_prefixes
933			.iter()
934			.filter(|p| *p != DEFAULT_CHILD_STORAGE_KEY_PREFIX)
935			.count() == 0
936		{
937			info!(
938				target: LOG_TARGET,
939				"since no prefix is filtered, the data for all pallets will be downloaded"
940			);
941			online_config.hashed_prefixes.push(vec![]);
942		}
943
944		Ok(())
945	}
946
947	/// Load the header for the target block. Tries each client until one succeeds.
948	async fn load_header(&self) -> Result<B::Header> {
949		let conn_manager = self.conn_manager()?;
950		let at = self.as_online().at_expected();
951
952		let num_clients = conn_manager.num_clients().await;
953		for i in 0..num_clients {
954			let client = conn_manager.get(i).await;
955			let result = with_timeout(
956				ChainApi::<(), _, B::Header, ()>::header(client.ws_client.as_ref(), Some(at)),
957				RPC_TIMEOUT,
958			)
959			.await;
960
961			match result {
962				Ok(Ok(Some(header))) => return Ok(header),
963				Ok(Ok(None)) => {
964					debug!(target: LOG_TARGET, "Client {i}: header returned None");
965				},
966				Ok(Err(e)) => {
967					debug!(target: LOG_TARGET, "Client {i}: header RPC error: {e:?}");
968				},
969				Err(()) => {
970					debug!(target: LOG_TARGET, "Client {i}: header timeout");
971				},
972			}
973		}
974
975		Err("rpc header failed on all clients")
976	}
977
978	/// Load the data from a remote server. The main code path is calling into `load_top_remote` and
979	/// `load_child_remote`.
980	///
981	/// Must be called after `init_remote_client`.
982	async fn load_remote_and_maybe_save(&mut self) -> Result<TestExternalities<HashingFor<B>>> {
983		let state_version = self.fetch_state_version().await?;
984		let mut pending_ext = TestExternalities::new_with_code_and_state(
985			Default::default(),
986			Default::default(),
987			self.overwrite_state_version.unwrap_or(state_version),
988		);
989
990		// Load data from the remote into `pending_ext`.
991		let top_kv = self.load_top_remote(&mut pending_ext).await?;
992		self.load_child_remote(&top_kv, &mut pending_ext).await?;
993
994		let header = self.load_header().await?;
995		let (raw_storage, computed_root) = pending_ext.into_raw_snapshot();
996
997		// Verify the downloaded state against the header's state root. Only a complete scrape can
998		// reproduce it, so partial scrapes are exempt. An overwritten state version is *not*
999		// exempt: a mismatch then means the overwrite is wrong, which callers opt out of via
1000		// `disable_root_check`.
1001		if self.as_online().disable_root_check {
1002			warn!(
1003				target: LOG_TARGET,
1004				"โš ๏ธ skipping storage root verification (disable_root_check is set)",
1005			);
1006		} else if self.is_complete_scrape() {
1007			let expected_root = *header.state_root();
1008			if computed_root != expected_root {
1009				error!(
1010					target: LOG_TARGET,
1011					"โŒ storage root mismatch: computed {computed_root:?}, expected {expected_root:?} \
1012					(from header). The downloaded state is incomplete or corrupted. If you are \
1013					overwriting the state version, set `disable_root_check` to bypass this check.",
1014				);
1015				return Err("storage root mismatch: downloaded state is incomplete or corrupted");
1016			}
1017			info!(target: LOG_TARGET, "โœ… storage root verified against header: {computed_root:?}");
1018		} else {
1019			debug!(
1020				target: LOG_TARGET,
1021				"skipping storage root verification for partial scrape (no full-state prefix)",
1022			);
1023		}
1024
1025		// If we need to save a snapshot, save the raw storage and root hash to the snapshot.
1026		if let Some(path) = self.as_online().state_snapshot.clone().map(|c| c.path) {
1027			let snapshot =
1028				Snapshot::<B>::new(state_version, raw_storage.clone(), computed_root, header);
1029			let encoded = snapshot.encode();
1030			info!(
1031				target: LOG_TARGET,
1032				"writing snapshot of {} bytes to {path:?}",
1033				encoded.len(),
1034			);
1035			std::fs::write(path, encoded).map_err(|_| "fs::write failed")?;
1036		}
1037
1038		// Return the externalities (reconstructed from verified snapshot)
1039		Ok(TestExternalities::from_raw_snapshot(
1040			raw_storage,
1041			computed_root,
1042			self.overwrite_state_version.unwrap_or(state_version),
1043		))
1044	}
1045
1046	async fn do_load_remote(&mut self) -> Result<RemoteExternalities<B>> {
1047		self.init_remote_client().await?;
1048		let inner_ext = self.load_remote_and_maybe_save().await?;
1049		Ok(RemoteExternalities { header: self.load_header().await?, inner_ext })
1050	}
1051
1052	fn do_load_offline(&mut self, config: OfflineConfig) -> Result<RemoteExternalities<B>> {
1053		let (header, inner_ext) = logging::with_elapsed(
1054			|| {
1055				info!(target: LOG_TARGET, "Loading snapshot from {:?}", &config.state_snapshot.path);
1056
1057				let Snapshot { header, state_version, raw_storage, storage_root, .. } =
1058					Snapshot::<B>::load(&config.state_snapshot.path)?;
1059				let inner_ext = TestExternalities::from_raw_snapshot(
1060					raw_storage,
1061					storage_root,
1062					self.overwrite_state_version.unwrap_or(state_version),
1063				);
1064
1065				Ok((header, inner_ext))
1066			},
1067			"Loading snapshot...",
1068			|_| "Loaded snapshot".into(),
1069		)?;
1070
1071		Ok(RemoteExternalities { inner_ext, header })
1072	}
1073
1074	pub(crate) async fn pre_build(mut self) -> Result<RemoteExternalities<B>> {
1075		let mut ext = match self.mode.clone() {
1076			Mode::Offline(config) => self.do_load_offline(config)?,
1077			Mode::Online(_) => self.do_load_remote().await?,
1078			Mode::OfflineOrElseOnline(offline_config, _) => {
1079				match self.do_load_offline(offline_config) {
1080					Ok(x) => x,
1081					Err(_) => self.do_load_remote().await?,
1082				}
1083			},
1084		};
1085
1086		// inject manual key values.
1087		if !self.hashed_key_values.is_empty() {
1088			info!(
1089				target: LOG_TARGET,
1090				"extending externalities with {} manually injected key-values",
1091				self.hashed_key_values.len()
1092			);
1093			ext.batch_insert(self.hashed_key_values.into_iter().map(|(k, v)| (k.0, v.0)));
1094		}
1095
1096		// exclude manual key values.
1097		if !self.hashed_blacklist.is_empty() {
1098			info!(
1099				target: LOG_TARGET,
1100				"excluding externalities from {} keys",
1101				self.hashed_blacklist.len()
1102			);
1103			for k in self.hashed_blacklist {
1104				ext.execute_with(|| sp_io::storage::clear(&k));
1105			}
1106		}
1107
1108		Ok(ext)
1109	}
1110}
1111
1112// Public methods
1113impl<B: BlockT> Builder<B>
1114where
1115	B::Hash: DeserializeOwned,
1116	B::Header: DeserializeOwned,
1117{
1118	/// Create a new builder.
1119	pub fn new() -> Self {
1120		Default::default()
1121	}
1122
1123	/// Inject a manual list of key and values to the storage.
1124	pub fn inject_hashed_key_value(mut self, injections: Vec<KeyValue>) -> Self {
1125		self.hashed_key_values.extend(injections);
1126		self
1127	}
1128
1129	/// Blacklist this hashed key from the final externalities. This is treated as-is, and should be
1130	/// pre-hashed.
1131	pub fn blacklist_hashed_key(mut self, hashed: &[u8]) -> Self {
1132		self.hashed_blacklist.push(hashed.to_vec());
1133		self
1134	}
1135
1136	/// Configure a state snapshot to be used.
1137	pub fn mode(mut self, mode: Mode<B::Hash>) -> Self {
1138		self.mode = mode;
1139		self
1140	}
1141
1142	/// The state version to use.
1143	pub fn overwrite_state_version(mut self, version: StateVersion) -> Self {
1144		self.overwrite_state_version = Some(version);
1145		self
1146	}
1147
1148	pub async fn build(self) -> Result<RemoteExternalities<B>> {
1149		let mut ext = self.pre_build().await?;
1150		ext.commit_all().unwrap();
1151
1152		info!(
1153			target: LOG_TARGET,
1154			"initialized state externalities with storage root {:?} and state_version {:?}",
1155			ext.as_backend().root(),
1156			ext.state_version
1157		);
1158
1159		Ok(ext)
1160	}
1161}
1162
1163#[cfg(test)]
1164mod test_prelude {
1165	pub(crate) use super::*;
1166	pub(crate) use sp_runtime::testing::{Block as RawBlock, MockCallU64};
1167	pub(crate) type UncheckedXt = sp_runtime::testing::TestXt<MockCallU64, ()>;
1168	pub(crate) type Block = RawBlock<UncheckedXt>;
1169
1170	pub(crate) fn init_logger() {
1171		sp_tracing::try_init_simple();
1172	}
1173}
1174
1175#[cfg(test)]
1176mod tests {
1177	use super::test_prelude::*;
1178
1179	#[tokio::test]
1180	async fn can_load_state_snapshot() {
1181		init_logger();
1182		Builder::<Block>::new()
1183			.mode(Mode::Offline(OfflineConfig {
1184				state_snapshot: SnapshotConfig::new("test_data/test.snap"),
1185			}))
1186			.build()
1187			.await
1188			.unwrap()
1189			.execute_with(|| {});
1190	}
1191
1192	#[tokio::test]
1193	async fn can_exclude_from_snapshot() {
1194		init_logger();
1195
1196		// get the first key from the snapshot file.
1197		let some_key = Builder::<Block>::new()
1198			.mode(Mode::Offline(OfflineConfig {
1199				state_snapshot: SnapshotConfig::new("test_data/test.snap"),
1200			}))
1201			.build()
1202			.await
1203			.expect("Can't read state snapshot file")
1204			.execute_with(|| {
1205				let key =
1206					sp_io::storage::next_key(&[]).expect("some key must exist in the snapshot");
1207				assert!(sp_io::storage::get(&key).is_some());
1208				key
1209			});
1210
1211		Builder::<Block>::new()
1212			.mode(Mode::Offline(OfflineConfig {
1213				state_snapshot: SnapshotConfig::new("test_data/test.snap"),
1214			}))
1215			.blacklist_hashed_key(&some_key)
1216			.build()
1217			.await
1218			.expect("Can't read state snapshot file")
1219			.execute_with(|| assert!(sp_io::storage::get(&some_key).is_none()));
1220	}
1221}
1222
1223#[cfg(all(test, feature = "remote-test"))]
1224mod remote_tests {
1225	use super::test_prelude::*;
1226	use frame_support::storage::KeyPrefixIterator;
1227	use std::{env, os::unix::fs::MetadataExt, path::Path};
1228
1229	fn endpoint() -> String {
1230		env::var("TEST_WS").unwrap_or_else(|_| DEFAULT_WS_ENDPOINT.to_string())
1231	}
1232
1233	#[tokio::test]
1234	async fn state_version_is_kept_and_can_be_altered() {
1235		const CACHE: &'static str = "state_version_is_kept_and_can_be_altered";
1236		init_logger();
1237
1238		// first, build a snapshot.
1239		let ext = Builder::<Block>::new()
1240			.mode(Mode::Online(OnlineConfig {
1241				transport_uris: vec![endpoint().clone()],
1242				pallets: vec!["Proxy".to_owned()],
1243				child_trie: false,
1244				state_snapshot: Some(SnapshotConfig::new(CACHE)),
1245				..Default::default()
1246			}))
1247			.build()
1248			.await
1249			.unwrap();
1250
1251		// now re-create the same snapshot.
1252		let cached_ext = Builder::<Block>::new()
1253			.mode(Mode::Offline(OfflineConfig { state_snapshot: SnapshotConfig::new(CACHE) }))
1254			.build()
1255			.await
1256			.unwrap();
1257
1258		assert_eq!(ext.state_version, cached_ext.state_version);
1259
1260		// now overwrite it
1261		let other = match ext.state_version {
1262			StateVersion::V0 => StateVersion::V1,
1263			StateVersion::V1 => StateVersion::V0,
1264		};
1265		let cached_ext = Builder::<Block>::new()
1266			.mode(Mode::Offline(OfflineConfig { state_snapshot: SnapshotConfig::new(CACHE) }))
1267			.overwrite_state_version(other)
1268			.build()
1269			.await
1270			.unwrap();
1271
1272		assert_eq!(cached_ext.state_version, other);
1273	}
1274
1275	#[tokio::test]
1276	async fn snapshot_block_hash_works() {
1277		const CACHE: &'static str = "snapshot_block_hash_works";
1278		init_logger();
1279
1280		// first, build a snapshot.
1281		let ext = Builder::<Block>::new()
1282			.mode(Mode::Online(OnlineConfig {
1283				transport_uris: vec![endpoint().clone()],
1284				pallets: vec!["Proxy".to_owned()],
1285				child_trie: false,
1286				state_snapshot: Some(SnapshotConfig::new(CACHE)),
1287				..Default::default()
1288			}))
1289			.build()
1290			.await
1291			.unwrap();
1292
1293		// now re-create the same snapshot.
1294		let cached_ext = Builder::<Block>::new()
1295			.mode(Mode::Offline(OfflineConfig { state_snapshot: SnapshotConfig::new(CACHE) }))
1296			.build()
1297			.await
1298			.unwrap();
1299
1300		assert_eq!(ext.header.hash(), cached_ext.header.hash());
1301	}
1302
1303	#[tokio::test]
1304	async fn child_keys_are_loaded() {
1305		const CACHE: &'static str = "snapshot_retains_storage";
1306		init_logger();
1307
1308		// This test does not rely on the remote endpoint having child tries. A synthetic child
1309		// storage entry is inserted locally and then asserted on.
1310		use sp_state_machine::Backend;
1311
1312		// Create an externality with child trie scraping enabled.
1313		let mut child_ext = Builder::<Block>::new()
1314			.mode(Mode::Online(OnlineConfig {
1315				transport_uris: vec![endpoint().clone()],
1316				pallets: vec!["Proxy".to_owned()],
1317				child_trie: true,
1318				state_snapshot: Some(SnapshotConfig::new(CACHE)),
1319				..Default::default()
1320			}))
1321			.build()
1322			.await
1323			.unwrap();
1324
1325		// Create an externality without looking for children keys
1326		let mut ext = Builder::<Block>::new()
1327			.mode(Mode::Online(OnlineConfig {
1328				transport_uris: vec![endpoint().clone()],
1329				pallets: vec!["Proxy".to_owned()],
1330				child_trie: false,
1331				state_snapshot: Some(SnapshotConfig::new(CACHE)),
1332				..Default::default()
1333			}))
1334			.build()
1335			.await
1336			.unwrap();
1337
1338		// Generate artificial child storage entry, to ensure the test's assertion is valid.
1339		let child_info = sp_core::storage::ChildInfo::new_default(b"test_child");
1340		let child_key: Vec<u8> = b"k1".to_vec();
1341		let child_value: Vec<u8> = b"v1".to_vec();
1342
1343		// Record the size of the underlying trie DB before inserting the child entry.
1344		let child_db_keys_before = child_ext.as_backend().backend_storage().keys().len();
1345
1346		// Insert child storage only into `child_ext`.
1347		child_ext.insert_child(child_info.clone(), child_key.clone(), child_value.clone());
1348
1349		// Assert: the child key exists only in the externalities where it is inserted.
1350		let child_backend = child_ext.as_backend();
1351		let backend = ext.as_backend();
1352		assert_eq!(
1353			child_backend.child_storage(&child_info, &child_key).unwrap(),
1354			Some(child_value)
1355		);
1356		assert_eq!(backend.child_storage(&child_info, &child_key).unwrap(), None);
1357
1358		// Structural assertion: insertion increased the underlying DB entry count.
1359		let child_db_keys_after = child_backend.backend_storage().keys().len();
1360		assert!(child_db_keys_after > child_db_keys_before);
1361	}
1362
1363	#[tokio::test]
1364	async fn offline_else_online_works() {
1365		const CACHE: &'static str = "offline_else_online_works_data";
1366		init_logger();
1367		// this shows that in the second run, we use the remote and create a snapshot.
1368		Builder::<Block>::new()
1369			.mode(Mode::OfflineOrElseOnline(
1370				OfflineConfig { state_snapshot: SnapshotConfig::new(CACHE) },
1371				OnlineConfig {
1372					transport_uris: vec![endpoint().clone()],
1373					pallets: vec!["Proxy".to_owned()],
1374					child_trie: false,
1375					state_snapshot: Some(SnapshotConfig::new(CACHE)),
1376					..Default::default()
1377				},
1378			))
1379			.build()
1380			.await
1381			.unwrap()
1382			.execute_with(|| {});
1383
1384		// this shows that in the second run, we are not using the remote
1385		Builder::<Block>::new()
1386			.mode(Mode::OfflineOrElseOnline(
1387				OfflineConfig { state_snapshot: SnapshotConfig::new(CACHE) },
1388				OnlineConfig {
1389					transport_uris: vec!["ws://non-existent:666".to_owned()],
1390					..Default::default()
1391				},
1392			))
1393			.build()
1394			.await
1395			.unwrap()
1396			.execute_with(|| {});
1397
1398		let to_delete = std::fs::read_dir(Path::new("."))
1399			.unwrap()
1400			.into_iter()
1401			.map(|d| d.unwrap())
1402			.filter(|p| p.path().file_name().unwrap_or_default() == CACHE)
1403			.collect::<Vec<_>>();
1404
1405		assert!(to_delete.len() == 1);
1406		std::fs::remove_file(to_delete[0].path()).unwrap();
1407	}
1408
1409	#[tokio::test]
1410	async fn can_build_one_small_pallet() {
1411		init_logger();
1412		Builder::<Block>::new()
1413			.mode(Mode::Online(OnlineConfig {
1414				transport_uris: vec![endpoint().clone()],
1415				pallets: vec!["Proxy".to_owned()],
1416				child_trie: false,
1417				..Default::default()
1418			}))
1419			.build()
1420			.await
1421			.unwrap()
1422			.execute_with(|| {});
1423	}
1424
1425	#[tokio::test]
1426	async fn can_build_few_pallet() {
1427		init_logger();
1428		Builder::<Block>::new()
1429			.mode(Mode::Online(OnlineConfig {
1430				transport_uris: vec![endpoint().clone()],
1431				pallets: vec!["Proxy".to_owned(), "Multisig".to_owned()],
1432				child_trie: false,
1433				..Default::default()
1434			}))
1435			.build()
1436			.await
1437			.unwrap()
1438			.execute_with(|| {});
1439	}
1440
1441	#[tokio::test(flavor = "multi_thread")]
1442	async fn can_create_snapshot() {
1443		const CACHE: &'static str = "can_create_snapshot";
1444		init_logger();
1445
1446		Builder::<Block>::new()
1447			.mode(Mode::Online(OnlineConfig {
1448				transport_uris: vec![endpoint().clone()],
1449				state_snapshot: Some(SnapshotConfig::new(CACHE)),
1450				pallets: vec!["Proxy".to_owned()],
1451				child_trie: false,
1452				..Default::default()
1453			}))
1454			.build()
1455			.await
1456			.unwrap()
1457			.execute_with(|| {});
1458
1459		let to_delete = std::fs::read_dir(Path::new("."))
1460			.unwrap()
1461			.into_iter()
1462			.map(|d| d.unwrap())
1463			.filter(|p| p.path().file_name().unwrap_or_default() == CACHE)
1464			.collect::<Vec<_>>();
1465
1466		assert!(to_delete.len() == 1);
1467		let to_delete = to_delete.first().unwrap();
1468		assert!(std::fs::metadata(to_delete.path()).unwrap().size() > 1);
1469		std::fs::remove_file(to_delete.path()).unwrap();
1470	}
1471
1472	#[tokio::test]
1473	async fn can_create_child_snapshot() {
1474		const CACHE: &'static str = "can_create_child_snapshot";
1475		init_logger();
1476		Builder::<Block>::new()
1477			.mode(Mode::Online(OnlineConfig {
1478				transport_uris: vec![endpoint().clone()],
1479				state_snapshot: Some(SnapshotConfig::new(CACHE)),
1480				pallets: vec!["Crowdloan".to_owned()],
1481				child_trie: true,
1482				..Default::default()
1483			}))
1484			.build()
1485			.await
1486			.unwrap()
1487			.execute_with(|| {});
1488
1489		let to_delete = std::fs::read_dir(Path::new("."))
1490			.unwrap()
1491			.into_iter()
1492			.map(|d| d.unwrap())
1493			.filter(|p| p.path().file_name().unwrap_or_default() == CACHE)
1494			.collect::<Vec<_>>();
1495
1496		assert!(to_delete.len() == 1);
1497		let to_delete = to_delete.first().unwrap();
1498		assert!(std::fs::metadata(to_delete.path()).unwrap().size() > 1);
1499		std::fs::remove_file(to_delete.path()).unwrap();
1500	}
1501
1502	#[tokio::test]
1503	async fn can_build_big_pallet() {
1504		if std::option_env!("TEST_WS").is_none() {
1505			return;
1506		}
1507		init_logger();
1508		Builder::<Block>::new()
1509			.mode(Mode::Online(OnlineConfig {
1510				transport_uris: vec![endpoint().clone()],
1511				pallets: vec!["Staking".to_owned()],
1512				child_trie: false,
1513				..Default::default()
1514			}))
1515			.build()
1516			.await
1517			.unwrap()
1518			.execute_with(|| {});
1519	}
1520
1521	#[tokio::test]
1522	async fn can_fetch_all() {
1523		if std::option_env!("TEST_WS").is_none() {
1524			return;
1525		}
1526		init_logger();
1527		Builder::<Block>::new()
1528			.mode(Mode::Online(OnlineConfig {
1529				transport_uris: vec![endpoint().clone()],
1530				..Default::default()
1531			}))
1532			.build()
1533			.await
1534			.unwrap()
1535			.execute_with(|| {});
1536	}
1537
1538	#[tokio::test]
1539	async fn can_fetch_in_parallel() {
1540		init_logger();
1541
1542		let mut builder = Builder::<Block>::new().mode(Mode::Online(OnlineConfig {
1543			transport_uris: vec![endpoint().clone()],
1544			..Default::default()
1545		}));
1546		builder.init_remote_client().await.unwrap();
1547
1548		let at = builder.as_online().at.unwrap();
1549
1550		// Test with a specific prefix
1551		let prefix = StorageKey(vec![13]);
1552		let para = builder.rpc_get_keys_parallel(&prefix, at, 4).await.unwrap();
1553		assert!(!para.is_empty(), "Should fetch some keys with prefix");
1554
1555		// Test with empty prefix (all keys)
1556		let prefix = StorageKey(vec![]);
1557		let para = builder.rpc_get_keys_parallel(&prefix, at, 8).await.unwrap();
1558		assert!(!para.is_empty(), "Should fetch some keys with empty prefix");
1559	}
1560
1561	#[tokio::test]
1562	#[ignore] // This test takes a long time, run with --ignored
1563	async fn bridge_hub_polkadot_storage_root_matches() {
1564		init_logger();
1565
1566		// Use multiple RPC providers for load distribution
1567		let endpoints = vec![
1568			"wss://bridge-hub-polkadot-rpc.n.dwellir.com",
1569			"wss://sys.ibp.network/bridgehub-polkadot",
1570			"wss://bridgehub-polkadot.api.onfinality.io/public",
1571			"wss://dot-rpc.stakeworld.io/bridgehub",
1572		];
1573
1574		info!(target: LOG_TARGET, "Connecting to Bridge Hub Polkadot using {} RPC providers", endpoints.len());
1575
1576		let mut ext = Builder::<Block>::new()
1577			.mode(Mode::Online(OnlineConfig {
1578				transport_uris: endpoints.into_iter().map(|e| e.to_owned()).collect(),
1579				child_trie: true,
1580				..Default::default()
1581			}))
1582			.build()
1583			.await
1584			.expect("Failed to build remote externalities");
1585
1586		// Get the computed storage root from our downloaded state
1587		let backend = ext.as_backend();
1588		let computed_root = *backend.root();
1589		// Get the expected storage root from the block header
1590		let expected_root = ext.header.state_root;
1591
1592		info!(
1593			target: LOG_TARGET,
1594			"Computed storage root: {:?}",
1595			computed_root
1596		);
1597		info!(
1598			target: LOG_TARGET,
1599			"Expected storage root (from header): {:?}",
1600			expected_root
1601		);
1602
1603		// The storage roots must match exactly - this proves we downloaded all keys correctly
1604		assert_eq!(
1605			computed_root, expected_root,
1606			"Storage root mismatch! Computed: {:?}, Expected: {:?}. \
1607			This indicates that not all keys were fetched or there were duplicates.",
1608			computed_root, expected_root
1609		);
1610
1611		ext.execute_with(|| {
1612			let key_count = KeyPrefixIterator::<()>::new(vec![], vec![], |_| Ok(())).count();
1613
1614			info!(target: LOG_TARGET, "Total keys in state: {}", key_count);
1615			assert!(key_count > 0, "Should have fetched some keys");
1616		});
1617
1618		info!(
1619			target: LOG_TARGET,
1620			"โœ… Storage root verification successful! All keys were fetched correctly."
1621		);
1622	}
1623
1624	#[tokio::test]
1625	#[ignore]
1626	async fn asset_hub_polkadot_storage_root_matches() {
1627		init_logger();
1628
1629		// Asset Hub carries the largest system-parachain state (incl. child tries), so a full
1630		// scrape is the strongest end-to-end check that the download matches the on-chain storage
1631		// root.
1632		let endpoints = vec![
1633			"wss://asset-hub-polkadot-rpc.dwellir.com",
1634			"wss://sys.ibp.network/asset-hub-polkadot",
1635			"wss://asset-hub-polkadot.api.onfinality.io/public",
1636			"wss://dot-rpc.stakeworld.io/assethub",
1637		];
1638
1639		info!(target: LOG_TARGET, "Connecting to Asset Hub Polkadot using {} RPC providers", endpoints.len());
1640
1641		let mut ext = Builder::<Block>::new()
1642			.mode(Mode::Online(OnlineConfig {
1643				transport_uris: endpoints.into_iter().map(|e| e.to_owned()).collect(),
1644				child_trie: true,
1645				..Default::default()
1646			}))
1647			.build()
1648			.await
1649			.expect("Failed to build remote externalities");
1650
1651		let backend = ext.as_backend();
1652		let computed_root = *backend.root();
1653		let expected_root = ext.header.state_root;
1654
1655		info!(target: LOG_TARGET, "Computed storage root: {:?}", computed_root);
1656		info!(target: LOG_TARGET, "Expected storage root (from header): {:?}", expected_root);
1657
1658		assert_eq!(
1659			computed_root, expected_root,
1660			"Storage root mismatch! Computed: {:?}, Expected: {:?}. \
1661			This indicates that not all keys were fetched or there were duplicates.",
1662			computed_root, expected_root
1663		);
1664
1665		ext.execute_with(|| {
1666			let key_count = KeyPrefixIterator::<()>::new(vec![], vec![], |_| Ok(())).count();
1667
1668			info!(target: LOG_TARGET, "Total keys in state: {}", key_count);
1669			assert!(key_count > 0, "Should have fetched some keys");
1670		});
1671
1672		info!(
1673			target: LOG_TARGET,
1674			"โœ… Storage root verification successful! All keys were fetched correctly."
1675		);
1676	}
1677
1678	#[tokio::test]
1679	async fn builder_fails_with_invalid_transport_uris() {
1680		init_logger();
1681
1682		// Using HTTP/HTTPS URIs should fail because Client::new() returns None for non-WS URIs
1683		let result = Builder::<Block>::new()
1684			.mode(Mode::Online(OnlineConfig {
1685				transport_uris: vec!["http://try-runtime.polkadot.io:443".to_string()],
1686				pallets: vec!["Proxy".to_owned()],
1687				..Default::default()
1688			}))
1689			.build()
1690			.await;
1691
1692		match result {
1693			Err(e) => assert_eq!(e, "At least one client must be provided"),
1694			Ok(_) => panic!("Expected error but got success"),
1695		}
1696
1697		// Multiple invalid URIs should also fail
1698		let result = Builder::<Block>::new()
1699			.mode(Mode::Online(OnlineConfig {
1700				transport_uris: vec![
1701					"http://try-runtime.polkadot.io:443".to_string(),
1702					"https://try-runtime.polkadot.io:443".to_string(),
1703					"garbage".to_string(),
1704				],
1705				pallets: vec!["Proxy".to_owned()],
1706				..Default::default()
1707			}))
1708			.build()
1709			.await;
1710
1711		match result {
1712			Err(e) => assert_eq!(e, "At least one client must be provided"),
1713			Ok(_) => panic!("Expected error but got success"),
1714		}
1715	}
1716}