1#[cfg(feature = "l2")]
2use crate::rlpx::l2::l2_connection::P2PBasedContext;
3#[cfg(not(feature = "l2"))]
4#[derive(Clone, Debug)]
5pub struct P2PBasedContext;
6use crate::{
7 discovery::{DiscoveryConfig, DiscoveryServer, DiscoveryServerError},
8 metrics::{CurrentStepValue, METRICS},
9 peer_table::{PeerData, PeerTable, PeerTableServerProtocol as _},
10 rlpx::{
11 connection::server::{PeerConnBroadcastSender, PeerConnection},
12 message::Message,
13 p2p::SUPPORTED_SNAP_CAPABILITIES,
14 },
15 tx_broadcaster::{TxBroadcaster, TxBroadcasterError},
16 types::{NetworkConfig, Node},
17};
18use ethrex_blockchain::Blockchain;
19use ethrex_common::H256;
20use ethrex_storage::Store;
21use secp256k1::SecretKey;
22use spawned_concurrency::tasks::ActorRef;
23use std::{
24 io,
25 net::SocketAddr,
26 sync::{Arc, atomic::Ordering},
27 time::Duration,
28};
29use tokio::net::{TcpListener, TcpSocket, UdpSocket};
30use tokio_util::task::TaskTracker;
31use tracing::{error, info};
32
33pub const MAX_MESSAGES_TO_BROADCAST: usize = 100000;
34
35#[derive(Clone, Debug)]
36pub struct P2PContext {
37 pub tracker: TaskTracker,
38 pub signer: SecretKey,
39 pub table: PeerTable,
40 pub storage: Store,
41 pub blockchain: Arc<Blockchain>,
42 pub(crate) broadcast: PeerConnBroadcastSender,
43 pub local_node: Node,
44 pub network_config: NetworkConfig,
46 pub client_version: String,
47 #[cfg(feature = "l2")]
48 pub based_context: Option<P2PBasedContext>,
49 pub tx_broadcaster: ActorRef<TxBroadcaster>,
50 pub initial_lookup_interval: f64,
51 pub(crate) inbound_admission: Arc<tokio::sync::Semaphore>,
55}
56
57const MAX_INBOUND_CONNECTIONS: usize = 150;
60
61impl P2PContext {
62 #[allow(clippy::too_many_arguments)]
63 pub fn new(
64 local_node: Node,
65 network_config: NetworkConfig,
66 tracker: TaskTracker,
67 signer: SecretKey,
68 peer_table: PeerTable,
69 storage: Store,
70 blockchain: Arc<Blockchain>,
71 client_version: String,
72 based_context: Option<P2PBasedContext>,
73 tx_broadcasting_time_interval: u64,
74 lookup_interval: f64,
75 ) -> Result<Self, NetworkError> {
76 let (channel_broadcast_send_end, _) = tokio::sync::broadcast::channel::<(
77 tokio::task::Id,
78 Arc<Message>,
79 )>(MAX_MESSAGES_TO_BROADCAST);
80
81 let tx_broadcaster = TxBroadcaster::spawn(
82 peer_table.clone(),
83 blockchain.clone(),
84 tx_broadcasting_time_interval,
85 )
86 .inspect_err(|e| {
87 error!("Failed to start Tx Broadcaster: {e}");
88 })?;
89
90 #[cfg(not(feature = "l2"))]
91 let _ = &based_context;
92
93 Ok(P2PContext {
94 local_node,
95 network_config,
96 tracker,
97 signer,
98 table: peer_table,
99 storage,
100 blockchain,
101 broadcast: channel_broadcast_send_end,
102 client_version,
103 #[cfg(feature = "l2")]
104 based_context,
105 tx_broadcaster,
106 initial_lookup_interval: lookup_interval,
107 inbound_admission: Arc::new(tokio::sync::Semaphore::new(MAX_INBOUND_CONNECTIONS)),
108 })
109 }
110}
111
112#[derive(Debug, thiserror::Error)]
113pub enum NetworkError {
114 #[error("Failed to start discovery server: {0}")]
115 DiscoveryError(#[from] DiscoveryServerError),
116 #[error("Failed to start Tx Broadcaster: {0}")]
117 TxBroadcasterError(#[from] TxBroadcasterError),
118 #[error("Failed to bind UDP socket: {0}")]
119 UdpSocketError(std::io::Error),
120}
121
122pub async fn start_network(
123 context: P2PContext,
124 bootnodes: Vec<Node>,
125 config: DiscoveryConfig,
126) -> Result<(), NetworkError> {
127 let udp_socket = Arc::new(
128 UdpSocket::bind(context.network_config.bind_udp_addr())
129 .await
130 .map_err(NetworkError::UdpSocketError)?,
131 );
132
133 DiscoveryServer::spawn(
134 context.storage.clone(),
135 context.local_node.clone(),
136 context.signer,
137 udp_socket,
138 context.table.clone(),
139 bootnodes,
140 DiscoveryConfig {
141 initial_lookup_interval: context.initial_lookup_interval,
142 ..config
143 },
144 )
145 .await
146 .inspect_err(|e| {
147 error!("Failed to start discovery server: {e}");
148 })?;
149
150 context.tracker.spawn(serve_p2p_requests(context.clone()));
151
152 Ok(())
153}
154
155pub(crate) async fn serve_p2p_requests(context: P2PContext) {
156 let tcp_addr = context.network_config.bind_tcp_addr();
157 let external_tcp_addr = context.local_node.tcp_addr();
158 let listener = match listener(tcp_addr) {
159 Ok(result) => result,
160 Err(e) => {
161 error!("Error opening tcp socket at {tcp_addr}: {e}. Stopping p2p server");
162 return;
163 }
164 };
165 loop {
166 let (stream, peer_addr) = match listener.accept().await {
167 Ok(result) => result,
168 Err(e) => {
169 error!("Error receiving data from tcp socket {tcp_addr}: {e}. Stopping p2p server");
170 return;
171 }
172 };
173
174 if external_tcp_addr == peer_addr {
175 continue;
177 }
178
179 let permit = match context.inbound_admission.clone().try_acquire_owned() {
183 Ok(permit) => permit,
184 Err(_) => {
185 tracing::debug!(peer = %peer_addr, "Inbound connection cap reached, dropping connection");
186 continue;
187 }
188 };
189
190 let _ = PeerConnection::spawn_as_receiver(context.clone(), peer_addr, stream, permit);
191 }
192}
193
194fn listener(tcp_addr: SocketAddr) -> Result<TcpListener, io::Error> {
195 let tcp_socket = match tcp_addr {
196 SocketAddr::V4(_) => TcpSocket::new_v4(),
197 SocketAddr::V6(_) => TcpSocket::new_v6(),
198 }?;
199 tcp_socket.set_reuseport(true).ok();
200 tcp_socket.set_reuseaddr(true).ok();
201 tcp_socket.bind(tcp_addr)?;
202
203 tcp_socket.listen(50)
204}
205
206pub async fn periodically_show_peer_stats(blockchain: Arc<Blockchain>, peer_table: PeerTable) {
207 periodically_show_peer_stats_during_syncing(blockchain, &peer_table).await;
208 periodically_show_peer_stats_after_sync(&peer_table).await;
209}
210
211#[derive(Default, Clone, Copy)]
213struct PhaseCounters {
214 headers: u64,
215 accounts: u64,
216 accounts_inserted: u64,
217 storage: u64,
218 storage_inserted: u64,
219 healed_accounts: u64,
220 healed_storage: u64,
221 bytecodes: u64,
222}
223
224impl PhaseCounters {
225 fn capture_current() -> Self {
226 Self {
227 headers: METRICS.downloaded_headers.get(),
228 accounts: METRICS.downloaded_account_tries.load(Ordering::Relaxed),
229 accounts_inserted: METRICS.account_tries_inserted.load(Ordering::Relaxed),
230 storage: METRICS.storage_leaves_downloaded.get(),
231 storage_inserted: METRICS.storage_leaves_inserted.get(),
232 healed_accounts: METRICS
233 .global_state_trie_leafs_healed
234 .load(Ordering::Relaxed),
235 healed_storage: METRICS
236 .global_storage_tries_leafs_healed
237 .load(Ordering::Relaxed),
238 bytecodes: METRICS.downloaded_bytecodes.load(Ordering::Relaxed),
239 }
240 }
241}
242
243pub async fn periodically_show_peer_stats_during_syncing(
244 blockchain: Arc<Blockchain>,
245 peer_table: &PeerTable,
246) {
247 let start = std::time::Instant::now();
248 let mut previous_step = CurrentStepValue::None;
249 let mut phase_start_time = std::time::Instant::now();
250 let mut sync_started_logged = false;
251
252 let mut phase_start = PhaseCounters::default();
254 let mut prev_interval = PhaseCounters::default();
256
257 loop {
258 if blockchain.is_synced() {
259 if !sync_started_logged {
260 info!("Node already has state; following chain via full sync");
261 return;
262 }
263 let total_elapsed = format_duration(start.elapsed());
265 let headers_downloaded = METRICS.downloaded_headers.get();
266 let accounts_downloaded = METRICS.downloaded_account_tries.load(Ordering::Relaxed);
267 let storage_downloaded = METRICS.storage_leaves_downloaded.get();
268 let bytecodes_downloaded = METRICS.downloaded_bytecodes.load(Ordering::Relaxed);
269 let healed_accounts = METRICS
270 .global_state_trie_leafs_healed
271 .load(Ordering::Relaxed);
272 let healed_storage = METRICS
273 .global_storage_tries_leafs_healed
274 .load(Ordering::Relaxed);
275
276 info!("");
277 info!(
278 "╭──────────────────────────────────────────────────────────────────────────────╮"
279 );
280 info!(
281 "│ SNAP SYNC COMPLETE │"
282 );
283 info!(
284 "├──────────────────────────────────────────────────────────────────────────────┤"
285 );
286 info!("│ {:<76}│", format!("Total time: {}", total_elapsed));
287 info!(
288 "├──────────────────────────────────────────────────────────────────────────────┤"
289 );
290 info!(
291 "│ Data summary: │"
292 );
293 let headers_accounts = format!(
294 " Headers: {:<14} │ Accounts: {}",
295 format_thousands(headers_downloaded),
296 format_thousands(accounts_downloaded)
297 );
298 info!("│ {:<76}│", headers_accounts);
299 let storage_bytecodes = format!(
300 " Storage: {:<14} │ Bytecodes: {}",
301 format_thousands(storage_downloaded),
302 format_thousands(bytecodes_downloaded)
303 );
304 info!("│ {:<76}│", storage_bytecodes);
305 let healed = format!(
306 " Healed: {} state paths + {} storage accounts",
307 format_thousands(healed_accounts),
308 format_thousands(healed_storage)
309 );
310 info!("│ {:<76}│", healed);
311 info!(
312 "╰──────────────────────────────────────────────────────────────────────────────╯"
313 );
314 return;
315 }
316
317 let metrics_enabled = *METRICS.enabled.lock().await;
318 if !metrics_enabled {
319 tokio::time::sleep(Duration::from_secs(1)).await;
320 continue;
321 }
322
323 let current_step = METRICS.current_step.get();
324 let peer_number = peer_table.peer_count().await.unwrap_or(0);
325
326 if !sync_started_logged && current_step != CurrentStepValue::None {
328 let sync_head_block = METRICS.sync_head_block.load(Ordering::Relaxed);
329 let sync_head_hash = *METRICS.sync_head_hash.lock().await;
330
331 if sync_head_block > 0 && sync_head_hash != H256::zero() {
333 let head_short = format!("{:x}", sync_head_hash);
334 let head_short = &head_short[..8.min(head_short.len())];
335
336 info!("");
337 info!("╭─────────────────────────────────────────────────────────────╮");
338 info!("│ {:<59} │", "SNAP SYNC STARTED");
339 let target_content = format!(
340 "Target: {}... (block #{})",
341 head_short,
342 format_thousands(sync_head_block)
343 );
344 info!("│ {:<59} │", target_content);
345 info!("│ {:<59} │", format!("Peers: {}", peer_number));
346 info!("╰─────────────────────────────────────────────────────────────╯");
347 sync_started_logged = true;
348 }
349 }
350
351 if !sync_started_logged {
353 tokio::time::sleep(Duration::from_secs(1)).await;
354 continue;
355 }
356
357 if current_step != previous_step && current_step != CurrentStepValue::None {
359 if previous_step != CurrentStepValue::None {
361 let phase_elapsed = phase_start_time.elapsed();
363 let total_elapsed = format_duration(start.elapsed());
364 log_phase_progress(
365 previous_step,
366 phase_elapsed,
367 &total_elapsed,
368 peer_number,
369 &prev_interval,
370 )
371 .await;
372
373 let phase_elapsed_str = format_duration(phase_start_time.elapsed());
374 log_phase_completion(
375 previous_step,
376 phase_elapsed_str,
377 &phase_metrics(previous_step, &phase_start).await,
378 );
379
380 #[cfg(feature = "metrics")]
382 push_sync_prometheus_metrics(previous_step);
383 }
384
385 phase_start_time = std::time::Instant::now();
387
388 #[cfg(feature = "metrics")]
390 {
391 let (_, phase_name) = phase_info(current_step);
392 let now = std::time::SystemTime::now()
393 .duration_since(std::time::UNIX_EPOCH)
394 .unwrap_or_default()
395 .as_secs();
396 ethrex_metrics::sync::METRICS_SYNC
397 .phase_start_timestamp
398 .with_label_values(&[phase_name])
399 .set(now as i64);
400 }
401
402 phase_start = PhaseCounters::capture_current();
404 prev_interval = phase_start;
405
406 log_phase_separator(current_step);
407 previous_step = current_step;
408 }
409
410 let phase_elapsed = phase_start_time.elapsed();
412 let total_elapsed = format_duration(start.elapsed());
413
414 log_phase_progress(
415 current_step,
416 phase_elapsed,
417 &total_elapsed,
418 peer_number,
419 &prev_interval,
420 )
421 .await;
422
423 #[cfg(feature = "metrics")]
425 {
426 push_sync_prometheus_metrics(current_step);
427 let diag = peer_table.get_peer_diagnostics().await.unwrap_or_default();
428 let snap_peers = diag
429 .iter()
430 .filter(|p| p.capabilities.iter().any(|c| c.starts_with("snap/")))
431 .count();
432 let eligible = diag.iter().filter(|p| p.eligible).count();
433 let inflight: i64 = diag.iter().map(|p| p.inflight_requests).sum();
434 ethrex_metrics::sync::METRICS_SYNC.set_snap_peers(snap_peers as i64);
435 ethrex_metrics::sync::METRICS_SYNC.set_eligible_peers(eligible as i64);
436 ethrex_metrics::sync::METRICS_SYNC.set_inflight_requests(inflight);
437 }
438
439 prev_interval = PhaseCounters::capture_current();
441
442 tokio::time::sleep(Duration::from_secs(10)).await;
443 }
444}
445
446fn phase_info(step: CurrentStepValue) -> (u8, &'static str) {
448 match step {
449 CurrentStepValue::DownloadingHeaders => (1, "BLOCK HEADERS"),
450 CurrentStepValue::RequestingAccountRanges => (2, "ACCOUNT RANGES"),
451 CurrentStepValue::InsertingAccountRanges | CurrentStepValue::InsertingAccountRangesNoDb => {
452 (3, "ACCOUNT INSERTION")
453 }
454 CurrentStepValue::RequestingStorageRanges => (4, "STORAGE RANGES"),
455 CurrentStepValue::InsertingStorageRanges => (5, "STORAGE INSERTION"),
456 CurrentStepValue::HealingState => (6, "STATE HEALING"),
457 CurrentStepValue::HealingStorage => (7, "STORAGE HEALING"),
458 CurrentStepValue::RequestingBytecodes => (8, "BYTECODES"),
459 CurrentStepValue::None => (0, "UNKNOWN"),
460 }
461}
462
463fn log_phase_separator(step: CurrentStepValue) {
464 let (phase_num, phase_name) = phase_info(step);
465 let header = format!("── PHASE {}/8: {} ", phase_num, phase_name);
466 let header_width = header.chars().count();
467 let padding_width = 80usize.saturating_sub(header_width);
468 let padding = "─".repeat(padding_width);
469 info!("");
470 info!("{}{}", header, padding);
471}
472
473fn log_phase_completion(step: CurrentStepValue, elapsed: String, summary: &str) {
474 let (_, phase_name) = phase_info(step);
475 info!("✓ {} complete: {} in {}", phase_name, summary, elapsed);
476}
477
478async fn phase_metrics(step: CurrentStepValue, phase_start: &PhaseCounters) -> String {
479 match step {
480 CurrentStepValue::DownloadingHeaders => {
481 let downloaded = METRICS
482 .downloaded_headers
483 .get()
484 .saturating_sub(phase_start.headers);
485 format!("{} headers", format_thousands(downloaded))
486 }
487 CurrentStepValue::RequestingAccountRanges => {
488 let downloaded = METRICS
489 .downloaded_account_tries
490 .load(Ordering::Relaxed)
491 .saturating_sub(phase_start.accounts);
492 format!("{} accounts", format_thousands(downloaded))
493 }
494 CurrentStepValue::InsertingAccountRanges | CurrentStepValue::InsertingAccountRangesNoDb => {
495 let inserted = METRICS
496 .account_tries_inserted
497 .load(Ordering::Relaxed)
498 .saturating_sub(phase_start.accounts_inserted);
499 format!("{} accounts inserted", format_thousands(inserted))
500 }
501 CurrentStepValue::RequestingStorageRanges => {
502 let downloaded = METRICS
503 .storage_leaves_downloaded
504 .get()
505 .saturating_sub(phase_start.storage);
506 format!("{} storage slots", format_thousands(downloaded))
507 }
508 CurrentStepValue::InsertingStorageRanges => {
509 let inserted = METRICS
510 .storage_leaves_inserted
511 .get()
512 .saturating_sub(phase_start.storage_inserted);
513 format!("{} storage slots inserted", format_thousands(inserted))
514 }
515 CurrentStepValue::HealingState => {
516 let healed = METRICS
517 .global_state_trie_leafs_healed
518 .load(Ordering::Relaxed)
519 .saturating_sub(phase_start.healed_accounts);
520 format!("{} state paths healed", format_thousands(healed))
521 }
522 CurrentStepValue::HealingStorage => {
523 let healed = METRICS
524 .global_storage_tries_leafs_healed
525 .load(Ordering::Relaxed)
526 .saturating_sub(phase_start.healed_storage);
527 format!("{} storage accounts healed", format_thousands(healed))
528 }
529 CurrentStepValue::RequestingBytecodes => {
530 let downloaded = METRICS
531 .downloaded_bytecodes
532 .load(Ordering::Relaxed)
533 .saturating_sub(phase_start.bytecodes);
534 format!("{} bytecodes", format_thousands(downloaded))
535 }
536 CurrentStepValue::None => String::new(),
537 }
538}
539
540const PROGRESS_INTERVAL_SECS: u64 = 30;
542
543async fn log_phase_progress(
544 step: CurrentStepValue,
545 phase_elapsed: Duration,
546 total_elapsed: &str,
547 peer_count: usize,
548 prev_interval: &PhaseCounters,
549) {
550 let phase_elapsed_str = format_duration(phase_elapsed);
551
552 let col1_width = 40;
554
555 match step {
556 CurrentStepValue::DownloadingHeaders => {
557 let headers_to_download = METRICS.sync_head_block.load(Ordering::Relaxed);
558 let headers_downloaded =
559 u64::min(METRICS.downloaded_headers.get(), headers_to_download);
560 let interval_downloaded = headers_downloaded.saturating_sub(prev_interval.headers);
561 let percentage = if headers_to_download == 0 {
562 0.0
563 } else {
564 (headers_downloaded as f64 / headers_to_download as f64) * 100.0
565 };
566 let rate = interval_downloaded / PROGRESS_INTERVAL_SECS;
567
568 let progress = progress_bar(percentage, 40);
569 info!(" {} {:>5.1}%", progress, percentage);
570 info!("");
571 let col1 = format!(
572 "Headers: {} / {}",
573 format_thousands(headers_downloaded),
574 format_thousands(headers_to_download)
575 );
576 info!(" {:<col1_width$} │ Elapsed: {}", col1, phase_elapsed_str);
577 let col1 = format!("Rate: {} headers/s", format_thousands(rate));
578 info!(" {:<col1_width$} │ Peers: {}", col1, peer_count);
579 info!(" Total time: {}", total_elapsed);
580 }
581 CurrentStepValue::RequestingAccountRanges => {
582 let accounts_downloaded = METRICS.downloaded_account_tries.load(Ordering::Relaxed);
583 let interval_downloaded = accounts_downloaded.saturating_sub(prev_interval.accounts);
584 let rate = interval_downloaded / PROGRESS_INTERVAL_SECS;
585
586 info!("");
587 let col1 = format!(
588 "Accounts fetched: {}",
589 format_thousands(accounts_downloaded)
590 );
591 info!(" {:<col1_width$} │ Elapsed: {}", col1, phase_elapsed_str);
592 let col1 = format!("Rate: {} accounts/s", format_thousands(rate));
593 info!(" {:<col1_width$} │ Peers: {}", col1, peer_count);
594 info!(" Total time: {}", total_elapsed);
595 }
596 CurrentStepValue::InsertingAccountRanges | CurrentStepValue::InsertingAccountRangesNoDb => {
597 let accounts_to_insert = METRICS.downloaded_account_tries.load(Ordering::Relaxed);
598 let accounts_inserted = METRICS.account_tries_inserted.load(Ordering::Relaxed);
599 let interval_inserted =
600 accounts_inserted.saturating_sub(prev_interval.accounts_inserted);
601 let percentage = if accounts_to_insert == 0 {
602 0.0
603 } else {
604 (accounts_inserted as f64 / accounts_to_insert as f64) * 100.0
605 };
606 let rate = interval_inserted / PROGRESS_INTERVAL_SECS;
607
608 let progress = progress_bar(percentage, 40);
609 info!(" {} {:>5.1}%", progress, percentage);
610 info!("");
611 let col1 = format!(
612 "Accounts: {} / {}",
613 format_thousands(accounts_inserted),
614 format_thousands(accounts_to_insert)
615 );
616 info!(" {:<col1_width$} │ Elapsed: {}", col1, phase_elapsed_str);
617 let col1 = format!("Rate: {} accounts/s", format_thousands(rate));
618 info!(" {:<col1_width$} │ Peers: {}", col1, peer_count);
619 info!(" Total time: {}", total_elapsed);
620 }
621 CurrentStepValue::RequestingStorageRanges => {
622 let storage_downloaded = METRICS.storage_leaves_downloaded.get();
623 let interval_downloaded = storage_downloaded.saturating_sub(prev_interval.storage);
624 let rate = interval_downloaded / PROGRESS_INTERVAL_SECS;
625
626 info!("");
627 let col1 = format!(
628 "Storage slots fetched: {}",
629 format_thousands(storage_downloaded)
630 );
631 info!(" {:<col1_width$} │ Elapsed: {}", col1, phase_elapsed_str);
632 let col1 = format!("Rate: {} slots/s", format_thousands(rate));
633 info!(" {:<col1_width$} │ Peers: {}", col1, peer_count);
634 info!(" Total time: {}", total_elapsed);
635 }
636 CurrentStepValue::InsertingStorageRanges => {
637 let storage_inserted = METRICS.storage_leaves_inserted.get();
638 let interval_inserted = storage_inserted.saturating_sub(prev_interval.storage_inserted);
639 let rate = interval_inserted / PROGRESS_INTERVAL_SECS;
640
641 info!("");
642 let col1 = format!(
643 "Storage slots inserted: {}",
644 format_thousands(storage_inserted)
645 );
646 info!(" {:<col1_width$} │ Elapsed: {}", col1, phase_elapsed_str);
647 let col1 = format!("Rate: {} slots/s", format_thousands(rate));
648 info!(" {:<col1_width$} │ Peers: {}", col1, peer_count);
649 info!(" Total time: {}", total_elapsed);
650 }
651 CurrentStepValue::HealingState => {
652 let healed = METRICS
653 .global_state_trie_leafs_healed
654 .load(Ordering::Relaxed);
655 let interval_healed = healed.saturating_sub(prev_interval.healed_accounts);
656 let rate = interval_healed / PROGRESS_INTERVAL_SECS;
657
658 info!("");
659 let col1 = format!("State paths healed: {}", format_thousands(healed));
660 info!(" {:<col1_width$} │ Elapsed: {}", col1, phase_elapsed_str);
661 let col1 = format!("Rate: {} paths/s", format_thousands(rate));
662 info!(" {:<col1_width$} │ Peers: {}", col1, peer_count);
663 info!(" Total time: {}", total_elapsed);
664 }
665 CurrentStepValue::HealingStorage => {
666 let healed = METRICS
667 .global_storage_tries_leafs_healed
668 .load(Ordering::Relaxed);
669 let interval_healed = healed.saturating_sub(prev_interval.healed_storage);
670 let rate = interval_healed / PROGRESS_INTERVAL_SECS;
671
672 info!("");
673 let col1 = format!("Storage accounts healed: {}", format_thousands(healed));
674 info!(" {:<col1_width$} │ Elapsed: {}", col1, phase_elapsed_str);
675 let col1 = format!("Rate: {} accounts/s", format_thousands(rate));
676 info!(" {:<col1_width$} │ Peers: {}", col1, peer_count);
677 info!(" Total time: {}", total_elapsed);
678 }
679 CurrentStepValue::RequestingBytecodes => {
680 let bytecodes_to_download = METRICS.bytecodes_to_download.load(Ordering::Relaxed);
681 let bytecodes_downloaded = METRICS.downloaded_bytecodes.load(Ordering::Relaxed);
682 let interval_downloaded = bytecodes_downloaded.saturating_sub(prev_interval.bytecodes);
683 let percentage = if bytecodes_to_download == 0 {
684 0.0
685 } else {
686 (bytecodes_downloaded as f64 / bytecodes_to_download as f64) * 100.0
687 };
688 let rate = interval_downloaded / PROGRESS_INTERVAL_SECS;
689
690 let progress = progress_bar(percentage, 40);
691 info!(" {} {:>5.1}%", progress, percentage);
692 info!("");
693 let col1 = format!(
694 "Bytecodes: {} / {}",
695 format_thousands(bytecodes_downloaded),
696 format_thousands(bytecodes_to_download)
697 );
698 info!(" {:<col1_width$} │ Elapsed: {}", col1, phase_elapsed_str);
699 let col1 = format!("Rate: {} codes/s", format_thousands(rate));
700 info!(" {:<col1_width$} │ Peers: {}", col1, peer_count);
701 info!(" Total time: {}", total_elapsed);
702 }
703 CurrentStepValue::None => {}
704 }
705}
706
707#[cfg(feature = "metrics")]
710fn push_sync_prometheus_metrics(step: CurrentStepValue) {
711 use ethrex_metrics::sync::METRICS_SYNC;
712 use std::sync::atomic::Ordering::Relaxed;
713
714 let (phase_num, _) = phase_info(step);
715 METRICS_SYNC.stage.set(phase_num as i64);
716 METRICS_SYNC
717 .pivot_block
718 .set(METRICS.sync_head_block.load(Relaxed) as i64);
719
720 let pivot_ts = METRICS.pivot_timestamp.load(Relaxed);
722 if pivot_ts > 0 {
723 METRICS_SYNC.pivot_timestamp.set(pivot_ts as i64);
724 }
725 if pivot_ts > 0 {
727 let now = std::time::SystemTime::now()
728 .duration_since(std::time::UNIX_EPOCH)
729 .unwrap_or_default()
730 .as_secs();
731 METRICS_SYNC
732 .pivot_age_seconds
733 .set(now.saturating_sub(pivot_ts) as i64);
734 }
735
736 match step {
737 CurrentStepValue::DownloadingHeaders => {
738 let total = METRICS.sync_head_block.load(Relaxed);
739 let downloaded = u64::min(METRICS.downloaded_headers.get(), total);
740 METRICS_SYNC.headers_downloaded.set(downloaded as i64);
741 METRICS_SYNC.headers_total.set(total as i64);
742 }
743 CurrentStepValue::RequestingAccountRanges => {
744 let downloaded = METRICS.downloaded_account_tries.load(Relaxed);
745 METRICS_SYNC.accounts_downloaded.set(downloaded as i64);
746 }
747 CurrentStepValue::InsertingAccountRanges | CurrentStepValue::InsertingAccountRangesNoDb => {
748 let total = METRICS.downloaded_account_tries.load(Relaxed);
749 let inserted = METRICS.account_tries_inserted.load(Relaxed);
750 METRICS_SYNC.accounts_downloaded.set(total as i64);
751 METRICS_SYNC.accounts_inserted.set(inserted as i64);
752 }
753 CurrentStepValue::RequestingStorageRanges => {
754 let downloaded = METRICS.storage_leaves_downloaded.get();
755 METRICS_SYNC.storage_downloaded.set(downloaded as i64);
756 }
757 CurrentStepValue::InsertingStorageRanges => {
758 let inserted = METRICS.storage_leaves_inserted.get();
759 METRICS_SYNC.storage_inserted.set(inserted as i64);
760 }
761 CurrentStepValue::HealingState => {
762 let healed = METRICS.global_state_trie_leafs_healed.load(Relaxed);
763 METRICS_SYNC.state_leaves_healed.set(healed as i64);
764 }
765 CurrentStepValue::HealingStorage => {
766 let healed = METRICS.global_storage_tries_leafs_healed.load(Relaxed);
767 METRICS_SYNC.storage_leaves_healed.set(healed as i64);
768 }
769 CurrentStepValue::RequestingBytecodes => {
770 let total = METRICS.bytecodes_to_download.load(Relaxed);
771 let downloaded = METRICS.downloaded_bytecodes.load(Relaxed);
772 METRICS_SYNC.bytecodes_downloaded.set(downloaded as i64);
773 METRICS_SYNC.bytecodes_total.set(total as i64);
774 }
775 CurrentStepValue::None => {}
776 }
777}
778
779fn progress_bar(percentage: f64, width: usize) -> String {
780 let clamped_percentage = percentage.clamp(0.0, 100.0);
781 let filled = ((clamped_percentage / 100.0) * width as f64) as usize;
782 let filled = filled.min(width);
783 let empty = width.saturating_sub(filled);
784 format!("{}{}", "▓".repeat(filled), "░".repeat(empty))
785}
786
787fn format_thousands(n: u64) -> String {
788 let s = n.to_string();
789 let mut result = String::new();
790 for (i, c) in s.chars().rev().enumerate() {
791 if i > 0 && i % 3 == 0 {
792 result.push(',');
793 }
794 result.push(c);
795 }
796 result.chars().rev().collect()
797}
798
799pub async fn periodically_show_peer_stats_after_sync(peer_table: &PeerTable) {
801 const INTERVAL_DURATION: tokio::time::Duration = tokio::time::Duration::from_secs(60);
802 let mut interval = tokio::time::interval(INTERVAL_DURATION);
803 loop {
804 let peers: Vec<PeerData> = peer_table.get_peers_data().await.unwrap_or(Vec::new());
806 let active_peers = peers
807 .iter()
808 .filter(|peer| -> bool { peer.connection.as_ref().is_some() })
809 .count();
810 let snap_active_peers = peers
811 .iter()
812 .filter(|peer| -> bool {
813 peer.connection.as_ref().is_some()
814 && SUPPORTED_SNAP_CAPABILITIES
815 .iter()
816 .any(|cap| peer.supported_capabilities.contains(cap))
817 })
818 .count();
819 info!("Peers: {active_peers} (snap-capable: {snap_active_peers})");
820 interval.tick().await;
821 }
822}
823
824fn format_duration(duration: Duration) -> String {
825 let total_seconds = duration.as_secs();
826 let hours = total_seconds / 3600;
827 let minutes = (total_seconds % 3600) / 60;
828 let seconds = total_seconds % 60;
829 format!("{hours:02}:{minutes:02}:{seconds:02}")
830}