1mod 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
75fn is_unknown_block_error(error: &str) -> bool {
80 error.contains("UnknownBlock")
81}
82
83fn retry_action(error: &str) -> RetryAction {
86 if is_unknown_block_error(error) {
87 RetryAction::Remove
88 } else {
89 RetryAction::Recreate
90 }
91}
92
93pub struct RemoteExternalities<B: BlockT> {
96 pub inner_ext: TestExternalities<HashingFor<B>>,
98 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#[derive(Clone)]
117pub struct Builder<B: BlockT> {
118 hashed_key_values: Vec<KeyValue>,
121 hashed_blacklist: Vec<Vec<u8>>,
123 mode: Mode<B::Hash>,
125 overwrite_state_version: Option<StateVersion>,
130 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
146impl<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 fn is_complete_scrape(&self) -> bool {
174 self.as_online().hashed_prefixes.iter().any(|p| p.is_empty())
175 }
176}
177
178impl<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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(keys.len(), storage_data.len());
603
604 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 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 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 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 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 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 let child_kv =
728 Self::rpc_child_get_storage_paged(client, prefixed_top_key, child_keys, at).await?;
729
730 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 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 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 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 async fn init_remote_client(&mut self) -> Result<()> {
897 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 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 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 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 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 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 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 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 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 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 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 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
1112impl<B: BlockT> Builder<B>
1114where
1115 B::Hash: DeserializeOwned,
1116 B::Header: DeserializeOwned,
1117{
1118 pub fn new() -> Self {
1120 Default::default()
1121 }
1122
1123 pub fn inject_hashed_key_value(mut self, injections: Vec<KeyValue>) -> Self {
1125 self.hashed_key_values.extend(injections);
1126 self
1127 }
1128
1129 pub fn blacklist_hashed_key(mut self, hashed: &[u8]) -> Self {
1132 self.hashed_blacklist.push(hashed.to_vec());
1133 self
1134 }
1135
1136 pub fn mode(mut self, mode: Mode<B::Hash>) -> Self {
1138 self.mode = mode;
1139 self
1140 }
1141
1142 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 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 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 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 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 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 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 use sp_state_machine::Backend;
1311
1312 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 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 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 let child_db_keys_before = child_ext.as_backend().backend_storage().keys().len();
1345
1346 child_ext.insert_child(child_info.clone(), child_key.clone(), child_value.clone());
1348
1349 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 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 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 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 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 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] async fn bridge_hub_polkadot_storage_root_matches() {
1564 init_logger();
1565
1566 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 let backend = ext.as_backend();
1588 let computed_root = *backend.root();
1589 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 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 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 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 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}