1use std::{
2 future::Future,
3 sync::{
4 atomic::{AtomicU64, Ordering},
5 Arc,
6 },
7};
8
9use futures::{FutureExt, StreamExt};
10use tokio::sync::{mpsc, Notify, OwnedSemaphorePermit, Semaphore};
11use tokio_util::sync::CancellationToken;
12use tower::{Service, ServiceExt};
13use tracing::instrument;
14
15use cuprate_consensus::{BlockChainContextRequest, BlockChainContextResponse, BlockchainContext};
16use cuprate_consensus_context::BlockchainContextService;
17use cuprate_helper::cast::usize_to_u64;
18use cuprate_p2p::{
19 block_downloader::{BlockBatch, BlockDownloaderConfig, ChainSvcRequest, ChainSvcResponse},
20 NetworkInterface, PeerSetRequest, PeerSetResponse,
21};
22use cuprate_p2p_core::{client::PeerSyncCallback, ClearNet, CoreSyncData, NetworkZone};
23
24use super::BlockchainManagerHandle;
25
26#[derive(Debug, thiserror::Error)]
28pub enum SyncerError {
29 #[error("Incoming block channel closed.")]
30 IncomingBlockChannelClosed,
31 #[error("One of our services returned an error: {0}.")]
32 ServiceError(#[from] tower::BoxError),
33 #[error("Sync permit semaphore closed unexpectedly: {0}.")]
34 SemaphoreClosed(#[from] tokio::sync::AcquireError),
35}
36
37#[derive(Debug, PartialEq)]
38enum SyncStatus {
39 NoPeers,
40 BehindPeers,
41 Synced,
42}
43
44pub struct BlockchainSyncer {
46 notify_syncer: Arc<Notify>,
47 synced_tx: Option<futures::channel::oneshot::Sender<()>>,
48 target_height: Arc<AtomicU64>,
49}
50
51impl BlockchainSyncer {
52 pub fn new(
54 handle: &BlockchainSyncerHandle,
55 synced_tx: futures::channel::oneshot::Sender<()>,
56 offline: bool,
57 ) -> Self {
58 let synced_tx = if offline {
59 #[expect(clippy::let_underscore_must_use)]
60 let _ = synced_tx.send(());
61 None
62 } else {
63 Some(synced_tx)
64 };
65
66 Self {
67 notify_syncer: Arc::clone(&handle.notify_syncer),
68 synced_tx,
69 target_height: Arc::clone(&handle.target_height),
70 }
71 }
72
73 #[instrument(name = "syncer", level = "debug", skip_all)]
75 #[expect(clippy::significant_drop_tightening)]
76 #[expect(clippy::too_many_arguments)]
77 pub async fn run<CN>(
78 mut self,
79 mut context_svc: BlockchainContextService,
80 our_chain: CN,
81 mut clearnet_interface: NetworkInterface<ClearNet>,
82 incoming_block_batch_tx: mpsc::Sender<(BlockBatch, Arc<OwnedSemaphorePermit>)>,
83 stop_current_block_downloader: Arc<Notify>,
84 block_downloader_config: BlockDownloaderConfig,
85 shutdown_token: CancellationToken,
86 ) -> Result<(), SyncerError>
87 where
88 CN: Service<
89 ChainSvcRequest<ClearNet>,
90 Response = ChainSvcResponse<ClearNet>,
91 Error = tower::BoxError,
92 > + Clone
93 + Send
94 + 'static,
95 CN::Future: Send + 'static,
96 {
97 tracing::info!("Starting blockchain syncer");
98 tracing::debug!("Waiting for new sync info in top sync channel");
99
100 let semaphore = Arc::new(Semaphore::new(1));
101 let mut sync_permit = Arc::new(Arc::clone(&semaphore).acquire_owned().await?);
102
103 loop {
104 tokio::select! {
105 biased;
106 () = shutdown_token.cancelled() => {
107 tracing::info!("Blockchain syncer shut down.");
108 return Ok(());
109 }
110 () = self.notify_syncer.notified() => {}
111 }
112
113 tracing::trace!("Checking connected peers to see if we are behind",);
114
115 match self
116 .check_sync_status(&mut context_svc, &mut clearnet_interface)
117 .await?
118 {
119 SyncStatus::BehindPeers => {}
120 SyncStatus::NoPeers => continue,
121 SyncStatus::Synced => {
122 if let Some(synced) = self.synced_tx.take() {
123 tracing::info!("Synchronised with the network.");
124 #[expect(clippy::let_underscore_must_use)]
125 let _ = synced.send(());
126 }
127 continue;
128 }
129 }
130
131 tracing::debug!(
132 "We are behind peers claimed cumulative difficulty, starting block downloader"
133 );
134 let mut block_batch_stream =
135 clearnet_interface.block_downloader(our_chain.clone(), block_downloader_config);
136
137 loop {
138 tokio::select! {
139 biased;
140 () = shutdown_token.cancelled() => {
141 tracing::info!("Blockchain syncer shut down.");
142 return Ok(());
143 }
144 () = stop_current_block_downloader.notified() => {
145 tracing::info!("Received stop signal, stopping block downloader");
146
147 drop(sync_permit);
148 sync_permit = Arc::new(Arc::clone(&semaphore).acquire_owned().await?);
149
150 self.notify_syncer.notify_one();
151 break;
152 }
153 batch = block_batch_stream.next() => {
154 let Some(batch) = batch else {
155 drop(sync_permit);
158 sync_permit = Arc::new(Arc::clone(&semaphore).acquire_owned().await?);
159
160 if self.check_sync_status(&mut context_svc, &mut clearnet_interface).await? == SyncStatus::Synced {
161 tracing::info!("Synchronised with the network.");
162 if let Some(synced) = self.synced_tx.take() {
163 #[expect(clippy::let_underscore_must_use)]
164 let _ = synced.send(());
165 }
166 }
167
168 break;
169 };
170
171 tracing::debug!("Got batch, len: {}", batch.blocks.len());
172 if incoming_block_batch_tx.send((batch, Arc::clone(&sync_permit))).await.is_err() {
173 if shutdown_token.is_cancelled() {
174 return Ok(());
175 }
176 return Err(SyncerError::IncomingBlockChannelClosed);
177 }
178 }
179 }
180 }
181 }
182 }
183
184 async fn check_sync_status(
186 &mut self,
187 context_svc: &mut BlockchainContextService,
188 clearnet_interface: &mut NetworkInterface<ClearNet>,
189 ) -> Result<SyncStatus, tower::BoxError> {
190 let PeerSetResponse::MostPoWSeen {
191 cumulative_difficulty,
192 height,
193 ..
194 } = clearnet_interface
195 .peer_set()
196 .ready()
197 .await?
198 .call(PeerSetRequest::MostPoWSeen)
199 .await?
200 else {
201 unreachable!();
202 };
203
204 if cumulative_difficulty == 0 {
205 self.target_height.store(0, Ordering::Relaxed);
206 return Ok(SyncStatus::NoPeers);
207 }
208
209 if cumulative_difficulty > context_svc.blockchain_context().cumulative_difficulty {
210 self.target_height
211 .store(usize_to_u64(height), Ordering::Relaxed);
212 return Ok(SyncStatus::BehindPeers);
213 }
214
215 self.target_height.store(0, Ordering::Relaxed);
216 Ok(SyncStatus::Synced)
217 }
218}
219
220#[derive(Clone)]
222pub struct BlockchainSyncerHandle {
223 notify_syncer: Arc<Notify>,
225 synced: futures::future::Shared<futures::channel::oneshot::Receiver<()>>,
227 target_height: Arc<AtomicU64>,
229}
230
231impl BlockchainSyncerHandle {
232 pub(crate) fn new() -> (Self, futures::channel::oneshot::Sender<()>) {
234 let (synced_tx, synced_rx) = futures::channel::oneshot::channel();
235
236 (
237 Self {
238 notify_syncer: Arc::new(Notify::new()),
239 synced: synced_rx.shared(),
240 target_height: Arc::new(AtomicU64::new(0)),
241 },
242 synced_tx,
243 )
244 }
245
246 pub fn target_height(&self) -> u64 {
248 self.target_height.load(Ordering::Relaxed)
249 }
250
251 pub fn wait_for_synced(
253 &self,
254 ) -> impl Future<Output = Result<(), futures::channel::oneshot::Canceled>> + 'static {
255 self.synced.clone()
256 }
257
258 pub(crate) fn callback(
260 &self,
261 context_svc: BlockchainContextService,
262 blockchain_manager: BlockchainManagerHandle,
263 ) -> PeerSyncCallback {
264 let sync_handle = self.clone();
265 let disconnect_handle = self.clone();
266
267 let on_sync = move |peer_csd: &CoreSyncData| {
268 let ctx = context_svc.blockchain_context_snapshot();
269
270 if peer_csd.cumulative_difficulty() == ctx.cumulative_difficulty
272 && sync_handle.synced.peek().is_none()
273 {
274 sync_handle.notify_syncer.notify_one();
275 }
276
277 if peer_csd.cumulative_difficulty() > ctx.cumulative_difficulty
279 && !(peer_csd.current_height.saturating_sub(1) == ctx.chain_height as u64
280 && blockchain_manager.is_block_being_handled(&peer_csd.top_id))
281 {
282 sync_handle.notify_syncer.notify_one();
283 }
284 };
285
286 let on_disconnect = move || disconnect_handle.notify_syncer.notify_one();
287
288 PeerSyncCallback::new(on_sync, on_disconnect)
289 }
290}