/ node / cdn / src / blocks.rs
blocks.rs
  1  // Copyright (c) 2025 ADnet Contributors
  2  // This file is part of the AlphaOS library.
  3  
  4  // Licensed under the Apache License, Version 2.0 (the "License");
  5  // you may not use this file except in compliance with the License.
  6  // You may obtain a copy of the License at:
  7  
  8  // http://www.apache.org/licenses/LICENSE-2.0
  9  
 10  // Unless required by applicable law or agreed to in writing, software
 11  // distributed under the License is distributed on an "AS IS" BASIS,
 12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  // See the License for the specific language governing permissions and
 14  // limitations under the License.
 15  
 16  // Avoid a false positive from clippy:
 17  // https://github.com/rust-lang/rust-clippy/issues/6446
 18  #![allow(clippy::await_holding_lock)]
 19  
 20  use alphaos_utilities::{SignalHandler, Stoppable};
 21  
 22  use alphavm::prelude::{
 23      Deserialize,
 24      DeserializeOwned,
 25      Ledger,
 26      Network,
 27      Serialize,
 28      block::Block,
 29      store::ConsensusStorage,
 30  };
 31  
 32  use anyhow::{Result, anyhow, bail};
 33  use colored::Colorize;
 34  #[cfg(feature = "locktick")]
 35  use locktick::{parking_lot::Mutex, tokio::Mutex as TMutex};
 36  #[cfg(not(feature = "locktick"))]
 37  use parking_lot::Mutex;
 38  use reqwest::Client;
 39  use std::{
 40      cmp,
 41      sync::{
 42          Arc,
 43          atomic::{AtomicBool, AtomicU32, Ordering},
 44      },
 45      time::{Duration, Instant},
 46  };
 47  #[cfg(not(feature = "locktick"))]
 48  use tokio::sync::Mutex as TMutex;
 49  use tokio::task::JoinHandle;
 50  
 51  /// The number of blocks per file.
 52  const BLOCKS_PER_FILE: u32 = 50;
 53  /// The desired number of concurrent requests to the CDN.
 54  const CONCURRENT_REQUESTS: u32 = 16;
 55  /// Maximum number of pending sync blocks.
 56  const MAXIMUM_PENDING_BLOCKS: u32 = BLOCKS_PER_FILE * CONCURRENT_REQUESTS * 2;
 57  /// Maximum number of attempts for a request to the CDN.
 58  const MAXIMUM_REQUEST_ATTEMPTS: u8 = 10;
 59  
 60  /// The CDN base url for ALPHA chain block sync.
 61  pub const CDN_BASE_URL: &str = "https://cdn.adnet.io/alpha/v0/blocks";
 62  
 63  /// Updates the metrics during CDN sync.
 64  #[cfg(feature = "metrics")]
 65  fn update_block_metrics(height: u32) {
 66      // Update the BFT height metric.
 67      crate::metrics::gauge(crate::metrics::bft::HEIGHT, height as f64);
 68  }
 69  
 70  pub type SyncResult = Result<u32, (u32, anyhow::Error)>;
 71  
 72  /// Manages the CDN sync task.
 73  ///
 74  /// This is used, for example, in alphaos_node_rest to query how
 75  /// far along the CDN sync is.
 76  pub struct CdnBlockSync {
 77      base_url: http::Uri,
 78      /// The background tasks that performs the sync operation.
 79      task: Mutex<Option<JoinHandle<SyncResult>>>,
 80      /// This flag will be set to true once the sync task has been successfully awaited.
 81      done: AtomicBool,
 82  }
 83  
 84  impl CdnBlockSync {
 85      /// Spawn a background task that loads blocks from a CDN into the ledger.
 86      ///
 87      /// On success, this function returns the completed block height.
 88      /// On failure, this function returns the last successful block height (if any), along with the error.
 89      pub fn new<N: Network, C: ConsensusStorage<N>>(
 90          base_url: http::Uri,
 91          ledger: Ledger<N, C>,
 92          stoppable: Arc<SignalHandler>,
 93      ) -> Self {
 94          let task = {
 95              let base_url = base_url.clone();
 96              tokio::spawn(async move { Self::worker(base_url, ledger, stoppable).await })
 97          };
 98  
 99          debug!("Started sync from CDN at {base_url}");
100          Self { done: AtomicBool::new(false), base_url, task: Mutex::new(Some(task)) }
101      }
102  
103      /// Did the CDN sync finish?
104      ///
105      /// Note: This can only return true if you call wait()
106      pub fn is_done(&self) -> bool {
107          self.done.load(Ordering::SeqCst)
108      }
109  
110      /// Wait for CDN sync to finish. Can only be called once.
111      pub async fn wait(&self) -> Result<SyncResult> {
112          let Some(hdl) = self.task.lock().take() else {
113              bail!("CDN task was already awaited");
114          };
115  
116          let result = hdl.await.map_err(|err| anyhow!("Failed to wait for CDN task: {err}"));
117          self.done.store(true, Ordering::SeqCst);
118          result
119      }
120  
121      async fn worker<N: Network, C: ConsensusStorage<N>>(
122          base_url: http::Uri,
123          ledger: Ledger<N, C>,
124          stoppable: Arc<dyn Stoppable>,
125      ) -> SyncResult {
126          // Fetch the node height.
127          let start_height = ledger.latest_height() + 1;
128          // Load the blocks from the CDN into the ledger.
129          let ledger_clone = ledger.clone();
130          let result = load_blocks(&base_url, start_height, None, stoppable, move |block: Block<N>| {
131              ledger_clone.advance_to_next_block(&block)
132          })
133          .await;
134  
135          // TODO (howardwu): Find a way to resolve integrity failures.
136          // If the sync failed, check the integrity of the ledger.
137          if let Err((completed_height, error)) = &result {
138              warn!("{error}");
139  
140              // If the sync made any progress, then check the integrity of the ledger.
141              if *completed_height != start_height {
142                  debug!("Synced the ledger up to block {completed_height}");
143  
144                  // Retrieve the latest height, according to the ledger.
145                  let node_height = *ledger.vm().block_store().heights().max().unwrap_or_default();
146                  // Check the integrity of the latest height.
147                  if &node_height != completed_height {
148                      return Err((*completed_height, anyhow!("The ledger height does not match the last sync height")));
149                  }
150  
151                  // Fetch the latest block from the ledger.
152                  if let Err(err) = ledger.get_block(node_height) {
153                      return Err((*completed_height, err));
154                  }
155              }
156  
157              Ok(*completed_height)
158          } else {
159              result
160          }
161      }
162  
163      pub async fn get_cdn_height(&self) -> anyhow::Result<u32> {
164          let client = Client::builder().use_rustls_tls().build()?;
165          cdn_height::<BLOCKS_PER_FILE>(&client, &self.base_url).await
166      }
167  }
168  
169  /// Loads blocks from a CDN and process them with the given function.
170  ///
171  /// On success, this function returns the completed block height.
172  /// On failure, this function returns the last successful block height (if any), along with the error.
173  pub async fn load_blocks<N: Network>(
174      base_url: &http::Uri,
175      start_height: u32,
176      end_height: Option<u32>,
177      stoppable: Arc<dyn Stoppable>,
178      process: impl FnMut(Block<N>) -> Result<()> + Clone + Send + Sync + 'static,
179  ) -> Result<u32, (u32, anyhow::Error)> {
180      // Create a Client to maintain a connection pool throughout the sync.
181      let client = match Client::builder().use_rustls_tls().build() {
182          Ok(client) => client,
183          Err(error) => {
184              return Err((start_height.saturating_sub(1), anyhow!("Failed to create a CDN request client - {error}")));
185          }
186      };
187  
188      // Fetch the CDN height.
189      let cdn_height = match cdn_height::<BLOCKS_PER_FILE>(&client, base_url).await {
190          Ok(cdn_height) => cdn_height,
191          Err(error) => return Err((start_height, error)),
192      };
193      // If the CDN height is less than the start height, return.
194      if cdn_height < start_height {
195          return Err((
196              start_height,
197              anyhow!("The given start height ({start_height}) must be less than the CDN height ({cdn_height})"),
198          ));
199      }
200  
201      // If the end height is not specified, set it to the CDN height.
202      // If the end height is greater than the CDN height, set the end height to the CDN height.
203      let end_height = cmp::min(end_height.unwrap_or(cdn_height), cdn_height);
204      // If the end height is less than the start height, return.
205      if end_height < start_height {
206          return Err((
207              start_height,
208              anyhow!("The given end height ({end_height}) must not be less than the start height ({start_height})"),
209          ));
210      }
211  
212      // Compute the CDN start height rounded down to the nearest multiple.
213      let cdn_start = start_height - (start_height % BLOCKS_PER_FILE);
214      // Set the CDN end height to the given end height.
215      let cdn_end = end_height;
216      // If the CDN range is empty, return.
217      if cdn_start >= cdn_end {
218          return Ok(cdn_end);
219      }
220  
221      // A collection of downloaded blocks pending insertion into the ledger.
222      let pending_blocks: Arc<TMutex<Vec<Block<N>>>> = Default::default();
223  
224      // Start a timer.
225      let timer = Instant::now();
226  
227      // Spawn a background task responsible for concurrent downloads.
228      let pending_blocks_clone = pending_blocks.clone();
229      let base_url = base_url.to_owned();
230  
231      {
232          let stoppable = stoppable.clone();
233          tokio::spawn(async move {
234              download_block_bundles(client, &base_url, cdn_start, cdn_end, pending_blocks_clone, stoppable).await;
235          });
236      }
237  
238      // A loop for inserting the pending blocks into the ledger.
239      let mut current_height = start_height.saturating_sub(1);
240      while current_height < end_height - 1 {
241          // If we are instructed to shut down, abort.
242          if stoppable.is_stopped() {
243              info!("Stopping block sync at {} - shutting down", current_height);
244              // We can shut down cleanly from here, as the node hasn't been started yet.
245              std::process::exit(0);
246          }
247  
248          let mut candidate_blocks = pending_blocks.lock().await;
249  
250          // Obtain the height of the nearest pending block.
251          let Some(next_height) = candidate_blocks.first().map(|b| b.height()) else {
252              debug!("No pending blocks yet");
253              drop(candidate_blocks);
254              tokio::time::sleep(Duration::from_secs(3)).await;
255              continue;
256          };
257  
258          // Wait if the nearest pending block is not the next one that can be inserted.
259          if next_height > current_height + 1 {
260              // There is a gap in pending blocks, we need to wait.
261              debug!("Waiting for the first relevant blocks ({} pending)", candidate_blocks.len());
262              drop(candidate_blocks);
263              tokio::time::sleep(Duration::from_secs(1)).await;
264              continue;
265          }
266  
267          // Obtain the first BLOCKS_PER_FILE applicable blocks.
268          let retained_blocks = candidate_blocks.split_off(BLOCKS_PER_FILE as usize);
269          let next_blocks = std::mem::replace(&mut *candidate_blocks, retained_blocks);
270          drop(candidate_blocks);
271  
272          // Initialize a temporary threadpool that can use the full CPU.
273          let threadpool = rayon::ThreadPoolBuilder::new().build().unwrap();
274  
275          // Attempt to advance the ledger using the CDN block bundle.
276          let mut process_clone = process.clone();
277          let stoppable_clone = stoppable.clone();
278          current_height = tokio::task::spawn_blocking(move || {
279              threadpool.install(|| {
280                  for block in next_blocks.into_iter().filter(|b| (start_height..end_height).contains(&b.height())) {
281                      // If we are instructed to shut down, abort.
282                      if stoppable_clone.is_stopped() {
283                          info!("Stopping block sync at {} - the node is shutting down", current_height);
284                          // We can shut down cleanly from here, as the node hasn't been started yet.
285                          std::process::exit(0);
286                      }
287  
288                      // Register the next block's height, as the block gets consumed next.
289                      let block_height = block.height();
290  
291                      // Insert the block into the ledger.
292                      process_clone(block)?;
293  
294                      // Update the current height.
295                      current_height = block_height;
296  
297                      // Update metrics.
298                      #[cfg(feature = "metrics")]
299                      update_block_metrics(current_height);
300  
301                      // Log the progress.
302                      log_progress::<BLOCKS_PER_FILE>(timer, current_height, cdn_start, cdn_end, "block");
303                  }
304  
305                  Ok(current_height)
306              })
307          })
308          .await
309          .map_err(|e| (current_height, e.into()))?
310          .map_err(|e| (current_height, e))?;
311      }
312  
313      Ok(current_height)
314  }
315  
316  async fn download_block_bundles<N: Network>(
317      client: Client,
318      base_url: &http::Uri,
319      cdn_start: u32,
320      cdn_end: u32,
321      pending_blocks: Arc<TMutex<Vec<Block<N>>>>,
322      stoppable: Arc<dyn Stoppable>,
323  ) {
324      // Keep track of the number of concurrent requests.
325      let active_requests: Arc<AtomicU32> = Default::default();
326  
327      let mut start = cdn_start;
328      while start < cdn_end - 1 {
329          // If we are instructed to shut down, stop downloading.
330          if stoppable.is_stopped() {
331              break;
332          }
333  
334          // Avoid collecting too many blocks in order to restrict memory use.
335          let num_pending_blocks = pending_blocks.lock().await.len();
336          if num_pending_blocks >= MAXIMUM_PENDING_BLOCKS as usize {
337              debug!("Maximum number of pending blocks reached ({num_pending_blocks}), waiting...");
338              tokio::time::sleep(Duration::from_secs(5)).await;
339              continue;
340          }
341  
342          // The number of concurrent requests is maintained at CONCURRENT_REQUESTS, unless the maximum
343          // number of pending blocks may be breached.
344          let active_request_count = active_requests.load(Ordering::Relaxed);
345          let num_requests =
346              cmp::min(CONCURRENT_REQUESTS, (MAXIMUM_PENDING_BLOCKS - num_pending_blocks as u32) / BLOCKS_PER_FILE)
347                  .saturating_sub(active_request_count);
348  
349          // Spawn concurrent requests for bundles of blocks.
350          for i in 0..num_requests {
351              let start = start + i * BLOCKS_PER_FILE;
352              let end = start + BLOCKS_PER_FILE;
353  
354              // If this request would breach the upper limit, stop downloading.
355              if end > cdn_end + BLOCKS_PER_FILE {
356                  debug!("Finishing network requests to the CDN...");
357                  break;
358              }
359  
360              let client_clone = client.clone();
361              let base_url_clone = base_url.clone();
362              let pending_blocks_clone = pending_blocks.clone();
363              let active_requests_clone = active_requests.clone();
364              let stoppable_clone = stoppable.clone();
365              tokio::spawn(async move {
366                  // Increment the number of active requests.
367                  active_requests_clone.fetch_add(1, Ordering::Relaxed);
368  
369                  let ctx = format!("blocks {start} to {end}");
370                  debug!("Requesting {ctx} (of {cdn_end})");
371  
372                  // Prepare the URL.
373                  let blocks_url = format!("{base_url_clone}/{start}.{end}.blocks");
374                  let ctx = format!("blocks {start} to {end}");
375                  // Download blocks, retrying on failure.
376                  let mut attempts = 0;
377                  let request_time = Instant::now();
378  
379                  loop {
380                      // Fetch the blocks.
381                      match cdn_get(client_clone.clone(), &blocks_url, &ctx).await {
382                          Ok::<Vec<Block<N>>, _>(blocks) => {
383                              // Keep the collection of pending blocks sorted by the height.
384                              let mut pending_blocks = pending_blocks_clone.lock().await;
385                              for block in blocks {
386                                  match pending_blocks.binary_search_by_key(&block.height(), |b| b.height()) {
387                                      Ok(_idx) => warn!("Found a duplicate pending block at height {}", block.height()),
388                                      Err(idx) => pending_blocks.insert(idx, block),
389                                  }
390                              }
391                              debug!("Received {ctx} {}", format!("(in {:.2?})", request_time.elapsed()).dimmed());
392                              break;
393                          }
394                          Err(error) => {
395                              // Increment the attempt counter, and wait with a linear backoff, or abort in
396                              // case the maximum number of attempts has been breached.
397                              attempts += 1;
398                              if attempts > MAXIMUM_REQUEST_ATTEMPTS {
399                                  warn!("Maximum number of requests to {blocks_url} reached - shutting down...");
400                                  stoppable_clone.stop();
401                                  break;
402                              }
403                              tokio::time::sleep(Duration::from_secs(attempts as u64 * 10)).await;
404                              warn!("{error} - retrying ({attempts} attempt(s) so far)");
405                          }
406                      }
407                  }
408  
409                  // Decrement the number of active requests.
410                  active_requests_clone.fetch_sub(1, Ordering::Relaxed);
411              });
412          }
413  
414          // Increase the starting block height for the subsequent requests.
415          start += BLOCKS_PER_FILE * num_requests;
416  
417          // A short sleep in order to allow some block processing to happen in the meantime.
418          tokio::time::sleep(Duration::from_secs(1)).await;
419      }
420  
421      debug!("Finished network requests to the CDN");
422  }
423  
424  /// Retrieves the CDN height with the given base URL.
425  ///
426  /// Note: This function decrements the tip by a few blocks, to ensure the
427  /// tip is not on a block that is not yet available on the CDN.
428  async fn cdn_height<const BLOCKS_PER_FILE: u32>(client: &Client, base_url: &http::Uri) -> Result<u32> {
429      // A representation of the 'latest.json' file object.
430      #[derive(Deserialize, Serialize, Debug)]
431      struct LatestState {
432          exclusive_height: u32,
433          inclusive_height: u32,
434          hash: String,
435      }
436      // Prepare the URL.
437      let latest_json_url = format!("{base_url}/latest.json");
438      // Send the request.
439      let response = match client.get(latest_json_url).send().await {
440          Ok(response) => response,
441          Err(error) => bail!("Failed to fetch the CDN height - {error}"),
442      };
443      // Parse the response.
444      let bytes = match response.bytes().await {
445          Ok(bytes) => bytes,
446          Err(error) => bail!("Failed to parse the CDN height response - {error}"),
447      };
448      // Parse the bytes for the string.
449      let latest_state_string = match bincode::deserialize::<String>(&bytes) {
450          Ok(string) => string,
451          Err(error) => bail!("Failed to deserialize the CDN height response - {error}"),
452      };
453      // Parse the string for the tip.
454      let tip = match serde_json::from_str::<LatestState>(&latest_state_string) {
455          Ok(latest) => latest.exclusive_height,
456          Err(error) => bail!("Failed to extract the CDN height response - {error}"),
457      };
458      // Decrement the tip by a few blocks to ensure the CDN is caught up.
459      let tip = tip.saturating_sub(10);
460      // Adjust the tip to the closest subsequent multiple of BLOCKS_PER_FILE.
461      Ok(tip - (tip % BLOCKS_PER_FILE) + BLOCKS_PER_FILE)
462  }
463  
464  /// Retrieves the objects from the CDN with the given URL.
465  async fn cdn_get<T: 'static + DeserializeOwned + Send>(client: Client, url: &str, ctx: &str) -> Result<T> {
466      // Fetch the bytes from the given URL.
467      let response = match client.get(url).send().await {
468          Ok(response) => response,
469          Err(error) => bail!("Failed to fetch {ctx} - {error}"),
470      };
471      // Parse the response.
472      let bytes = match response.bytes().await {
473          Ok(bytes) => bytes,
474          Err(error) => bail!("Failed to parse {ctx} - {error}"),
475      };
476  
477      // Parse the objects.
478      match tokio::task::spawn_blocking(move || bincode::deserialize::<T>(&bytes)).await {
479          Ok(Ok(objects)) => Ok(objects),
480          Ok(Err(error)) => bail!("Failed to deserialize {ctx} - {error}"),
481          Err(error) => bail!("Failed to join task for {ctx} - {error}"),
482      }
483  }
484  
485  /// Converts a duration into a string that humans can read easily.
486  ///
487  /// # Output
488  /// The output remains to be accurate but not too detailed (to reduce noise in the log).
489  /// It will give at most two levels of granularity, e.g., days and hours,
490  /// and only shows seconds if less than a minute remains.
491  fn to_human_readable_duration(duration: Duration) -> String {
492      // TODO: simplify this once the duration_constructors feature is stable
493      // See: https://github.com/rust-lang/rust/issues/140881
494      const SECS_PER_MIN: u64 = 60;
495      const MINS_PER_HOUR: u64 = 60;
496      const SECS_PER_HOUR: u64 = SECS_PER_MIN * MINS_PER_HOUR;
497      const HOURS_PER_DAY: u64 = 24;
498      const SECS_PER_DAY: u64 = SECS_PER_HOUR * HOURS_PER_DAY;
499  
500      let duration = duration.as_secs();
501  
502      if duration < 1 {
503          "less than one second".to_string()
504      } else if duration < SECS_PER_MIN {
505          format!("{duration} seconds")
506      } else if duration < SECS_PER_HOUR {
507          format!("{} minutes", duration / SECS_PER_MIN)
508      } else if duration < SECS_PER_DAY {
509          let mins = duration / SECS_PER_MIN;
510          format!("{hours} hours and {remainder} minutes", hours = mins / 60, remainder = mins % 60)
511      } else {
512          let days = duration / SECS_PER_DAY;
513          let hours = (duration % SECS_PER_DAY) / SECS_PER_HOUR;
514          format!("{days} days and {hours} hours")
515      }
516  }
517  
518  /// Logs the progress of the sync.
519  fn log_progress<const OBJECTS_PER_FILE: u32>(
520      timer: Instant,
521      current_index: u32,
522      cdn_start: u32,
523      mut cdn_end: u32,
524      object_name: &str,
525  ) {
526      debug_assert!(cdn_start <= cdn_end);
527      debug_assert!(current_index <= cdn_end);
528      debug_assert!(cdn_end >= 1);
529  
530      // Subtract 1, as the end of the range is exclusive.
531      cdn_end -= 1;
532  
533      // Compute the percentage completed of this particular sync.
534      let sync_percentage =
535          (current_index.saturating_sub(cdn_start) * 100).checked_div(cdn_end.saturating_sub(cdn_start)).unwrap_or(100);
536  
537      // Compute the number of files processed so far.
538      let num_files_done = 1 + (current_index - cdn_start) / OBJECTS_PER_FILE;
539      // Compute the number of files remaining.
540      let num_files_remaining = 1 + (cdn_end.saturating_sub(current_index)) / OBJECTS_PER_FILE;
541      // Compute the milliseconds per file.
542      let millis_per_file = timer.elapsed().as_millis() / num_files_done as u128;
543      // Compute the heuristic slowdown factor (in millis).
544      let slowdown = 100 * num_files_remaining as u128;
545      // Compute the time remaining (in millis).
546      let time_remaining = {
547          let remaining = num_files_remaining as u128 * millis_per_file + slowdown;
548          to_human_readable_duration(Duration::from_secs((remaining / 1000) as u64))
549      };
550      // Prepare the estimate message (in secs).
551      let estimate = format!("(started at height {cdn_start}, est. {time_remaining} remaining)");
552      // Log the progress.
553      info!(
554          "Reached {object_name} {current_index} of {cdn_end} - Sync is {sync_percentage}% complete {}",
555          estimate.dimmed()
556      );
557  }
558  
559  #[cfg(test)]
560  mod tests {
561      use super::{BLOCKS_PER_FILE, CDN_BASE_URL, cdn_height, load_blocks, log_progress};
562  
563      use alphaos_utilities::SimpleStoppable;
564  
565      use alphavm::prelude::{MainnetV0, block::Block};
566  
567      use http::Uri;
568      use parking_lot::RwLock;
569      use std::{sync::Arc, time::Instant};
570  
571      type CurrentNetwork = MainnetV0;
572  
573      fn check_load_blocks(start: u32, end: Option<u32>, expected: usize) {
574          let blocks = Arc::new(RwLock::new(Vec::new()));
575          let blocks_clone = blocks.clone();
576          let process = move |block: Block<CurrentNetwork>| {
577              blocks_clone.write().push(block);
578              Ok(())
579          };
580  
581          let testnet_cdn_url = Uri::try_from(format!("{CDN_BASE_URL}/mainnet")).unwrap();
582  
583          let rt = tokio::runtime::Runtime::new().unwrap();
584          rt.block_on(async {
585              let completed_height =
586                  load_blocks(&testnet_cdn_url, start, end, SimpleStoppable::new(), process).await.unwrap();
587              assert_eq!(blocks.read().len(), expected);
588              if expected > 0 {
589                  assert_eq!(blocks.read().last().unwrap().height(), completed_height);
590              }
591              // Check they are sequential.
592              for (i, block) in blocks.read().iter().enumerate() {
593                  assert_eq!(block.height(), start + i as u32);
594              }
595          });
596      }
597  
598      #[test]
599      fn test_load_blocks_0_to_50() {
600          let start_height = 0;
601          let end_height = Some(50);
602          check_load_blocks(start_height, end_height, 50);
603      }
604  
605      #[test]
606      fn test_load_blocks_50_to_100() {
607          let start_height = 50;
608          let end_height = Some(100);
609          check_load_blocks(start_height, end_height, 50);
610      }
611  
612      #[test]
613      fn test_load_blocks_0_to_123() {
614          let start_height = 0;
615          let end_height = Some(123);
616          check_load_blocks(start_height, end_height, 123);
617      }
618  
619      #[test]
620      fn test_load_blocks_46_to_234() {
621          let start_height = 46;
622          let end_height = Some(234);
623          check_load_blocks(start_height, end_height, 188);
624      }
625  
626      #[test]
627      fn test_cdn_height() {
628          let rt = tokio::runtime::Runtime::new().unwrap();
629          let client = reqwest::Client::builder().use_rustls_tls().build().unwrap();
630          let testnet_cdn_url = Uri::try_from(format!("{CDN_BASE_URL}/mainnet")).unwrap();
631          rt.block_on(async {
632              let height = cdn_height::<BLOCKS_PER_FILE>(&client, &testnet_cdn_url).await.unwrap();
633              assert!(height > 0);
634          });
635      }
636  
637      #[test]
638      fn test_log_progress() {
639          // This test sanity checks that basic arithmetic is correct (i.e. no divide by zero, etc.).
640          let timer = Instant::now();
641          let cdn_start = 0;
642          let cdn_end = 100;
643          let object_name = "blocks";
644          log_progress::<10>(timer, 0, cdn_start, cdn_end, object_name);
645          log_progress::<10>(timer, 10, cdn_start, cdn_end, object_name);
646          log_progress::<10>(timer, 20, cdn_start, cdn_end, object_name);
647          log_progress::<10>(timer, 30, cdn_start, cdn_end, object_name);
648          log_progress::<10>(timer, 40, cdn_start, cdn_end, object_name);
649          log_progress::<10>(timer, 50, cdn_start, cdn_end, object_name);
650          log_progress::<10>(timer, 60, cdn_start, cdn_end, object_name);
651          log_progress::<10>(timer, 70, cdn_start, cdn_end, object_name);
652          log_progress::<10>(timer, 80, cdn_start, cdn_end, object_name);
653          log_progress::<10>(timer, 90, cdn_start, cdn_end, object_name);
654          log_progress::<10>(timer, 100, cdn_start, cdn_end, object_name);
655      }
656  }