subcoin_informant/
lib.rs

1//! # Subcoin Informant
2//!
3//! This crate is a fork of `sc-informant` for displaying the Subcoin network sync progress.
4
5mod display;
6
7use bitcoin::BlockHash;
8use console::style;
9use futures::prelude::*;
10use futures_timer::Delay;
11use sc_client_api::{AuxStore, BlockchainEvents, ClientInfo, UsageProvider};
12use sp_blockchain::{HeaderBackend, HeaderMetadata};
13use sp_runtime::traits::{Block as BlockT, Header};
14use std::collections::VecDeque;
15use std::fmt::Display;
16use std::ops::Deref;
17use std::sync::Arc;
18use std::time::Duration;
19use subcoin_network::NetworkApi;
20use subcoin_primitives::BackendExt;
21use tracing::{debug, info, trace};
22
23/// Extended [`ClientInfo`].
24#[derive(Debug)]
25struct ClientInfoExt<Block: BlockT> {
26    info: ClientInfo<Block>,
27    best_bitcoin_hash: BlockHash,
28    finalized_bitcoin_hash: BlockHash,
29}
30
31impl<Block: BlockT> Deref for ClientInfoExt<Block> {
32    type Target = ClientInfo<Block>;
33    fn deref(&self) -> &Self::Target {
34        &self.info
35    }
36}
37
38/// Creates a stream that returns a new value every `duration`.
39fn interval(duration: Duration) -> impl Stream<Item = ()> + Unpin {
40    futures::stream::unfold((), move |_| Delay::new(duration).map(|_| Some(((), ())))).map(drop)
41}
42
43/// Builds the informant and returns a `Future` that drives the informant.
44pub async fn build<B: BlockT, C>(client: Arc<C>, network_api: Arc<dyn NetworkApi>)
45where
46    C: UsageProvider<B> + HeaderMetadata<B> + BlockchainEvents<B> + HeaderBackend<B> + AuxStore,
47    <C as HeaderMetadata<B>>::Error: Display,
48{
49    let mut display = display::InformantDisplay::new();
50
51    let net_client = client.clone();
52
53    let display_notifications = interval(Duration::from_millis(5000))
54        .filter_map(|_| async { network_api.status().await })
55        .for_each({
56            move |net_status| {
57                let info = net_client.usage_info();
58                if let Some(ref usage) = info.usage {
59                    trace!(target: "usage", "Usage statistics: {}", usage);
60                } else {
61                    trace!(
62                        target: "usage",
63                        "Usage statistics not displayed as backend does not provide it",
64                    )
65                }
66
67                let best_bitcoin_hash = net_client
68                    .bitcoin_block_hash_for(info.chain.best_hash)
69                    .expect("Best bitcoin hash must exist; qed");
70
71                let finalized_bitcoin_hash = net_client
72                    .bitcoin_block_hash_for(info.chain.finalized_hash)
73                    .expect("Finalized bitcoin hash must exist; qed");
74
75                let client_info_ext = ClientInfoExt {
76                    info,
77                    best_bitcoin_hash,
78                    finalized_bitcoin_hash,
79                };
80
81                display.display(client_info_ext, net_status);
82                future::ready(())
83            }
84        });
85
86    futures::select! {
87        () = display_notifications.fuse() => (),
88        () = display_block_import(client, &network_api).fuse() => (),
89    };
90}
91
92async fn display_block_import<B: BlockT, C>(client: Arc<C>, network_api: &Arc<dyn NetworkApi>)
93where
94    C: UsageProvider<B> + HeaderMetadata<B> + BlockchainEvents<B>,
95    <C as HeaderMetadata<B>>::Error: Display,
96{
97    let mut last_best = {
98        let info = client.usage_info();
99        Some((info.chain.best_number, info.chain.best_hash))
100    };
101
102    // Hashes of the last blocks we have seen at import.
103    let mut last_blocks = VecDeque::new();
104    let max_blocks_to_track = 100;
105
106    while let Some(n) = client.import_notification_stream().next().await {
107        // detect and log reorganizations.
108        if let Some((ref last_num, ref last_hash)) = last_best {
109            if n.header.parent_hash() != last_hash && n.is_new_best {
110                let maybe_ancestor =
111                    sp_blockchain::lowest_common_ancestor(&*client, *last_hash, n.hash);
112
113                match maybe_ancestor {
114                    Ok(ref ancestor) if ancestor.hash != *last_hash => info!(
115                        "♻️  Reorg on #{},{} to #{},{}, common ancestor #{},{}",
116                        style(last_num).red().bold(),
117                        last_hash,
118                        style(n.header.number()).green().bold(),
119                        n.hash,
120                        style(ancestor.number).white().bold(),
121                        ancestor.hash,
122                    ),
123                    Ok(_) => {}
124                    Err(e) => debug!("Error computing tree route: {}", e),
125                }
126            }
127        }
128
129        if network_api.is_major_syncing() {
130            continue;
131        }
132
133        if n.is_new_best {
134            last_best = Some((*n.header.number(), n.hash));
135        }
136
137        // If we already printed a message for a given block recently,
138        // we should not print it again.
139        if !last_blocks.contains(&n.hash) {
140            last_blocks.push_back(n.hash);
141
142            if last_blocks.len() > max_blocks_to_track {
143                last_blocks.pop_front();
144            }
145
146            let best_indicator = if n.is_new_best { "🏆" } else { "🆕" };
147            info!(
148                target: "subcoin",
149                "{best_indicator} Imported #{} ({} → {})",
150                style(n.header.number()).white().bold(),
151                n.header.parent_hash(),
152                n.hash,
153            );
154        }
155    }
156}