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 }