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