Skip to main content

sui_network/state_sync/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Peer-to-peer data synchronization of checkpoints.
5//!
6//! This StateSync module is responsible for the synchronization and dissemination of checkpoints
7//! and the transactions, and their effects, contained within. This module is *not* responsible for
8//! the execution of the transactions included in a checkpoint, that process is left to another
9//! component in the system.
10//!
11//! # High-level Overview of StateSync
12//!
13//! StateSync discovers new checkpoints via a few different sources:
14//! 1. If this node is a Validator, checkpoints will be produced via consensus at which point
15//!    consensus can notify state-sync of the new checkpoint via [Handle::send_checkpoint].
16//! 2. A peer notifies us of the latest checkpoint which they have synchronized. State-Sync will
17//!    also periodically query its peers to discover what their latest checkpoint is.
18//!
19//! We keep track of two different watermarks:
20//! * highest_verified_checkpoint - This is the highest checkpoint header that we've locally
21//!   verified. This indicated that we have in our persistent store (and have verified) all
22//!   checkpoint headers up to and including this value.
23//! * highest_synced_checkpoint - This is the highest checkpoint that we've fully synchronized,
24//!   meaning we've downloaded and have in our persistent stores all of the transactions, and their
25//!   effects (but not the objects), for all checkpoints up to and including this point. This is
26//!   the watermark that is shared with other peers, either via notification or when they query for
27//!   our latest checkpoint, and is intended to be used as a guarantee of data availability.
28//!
29//! The `PeerHeights` struct is used to track the highest_synced_checkpoint watermark for all of
30//! our peers.
31//!
32//! When a new checkpoint is discovered, and we've determined that it is higher than our
33//! highest_verified_checkpoint, then StateSync will kick off a task to synchronize and verify all
34//! checkpoints between our highest_synced_checkpoint and the newly discovered checkpoint. This
35//! process is done by querying one of our peers for the checkpoints we're missing (using the
36//! `PeerHeights` struct as a way to intelligently select which peers have the data available for
37//! us to query) at which point we will locally verify the signatures on the checkpoint header with
38//! the appropriate committee (based on the epoch). As checkpoints are verified, the
39//! highest_synced_checkpoint watermark will be ratcheted up.
40//!
41//! Once we've ratcheted up our highest_verified_checkpoint, and if it is higher than
42//! highest_synced_checkpoint, StateSync will then kick off a task to synchronize the contents of
43//! all of the checkpoints from highest_synced_checkpoint..=highest_verified_checkpoint. After the
44//! contents of each checkpoint is fully downloaded, StateSync will update our
45//! highest_synced_checkpoint watermark and send out a notification on a broadcast channel
46//! indicating that a new checkpoint has been fully downloaded. Notifications on this broadcast
47//! channel will always be made in order. StateSync will also send out a notification to its peers
48//! of the newly synchronized checkpoint so that it can help other peers synchronize.
49
50use anemo::{PeerId, Request, Response, Result, types::PeerEvent};
51use futures::{FutureExt, StreamExt, stream::FuturesOrdered};
52use rand::Rng;
53use std::{
54    collections::{HashMap, VecDeque},
55    sync::{Arc, RwLock},
56    time::{Duration, Instant},
57};
58use sui_config::p2p::StateSyncConfig;
59use sui_types::{
60    committee::Committee,
61    digests::CheckpointDigest,
62    messages_checkpoint::{
63        CertifiedCheckpointSummary as Checkpoint, CheckpointSequenceNumber, EndOfEpochData,
64        VerifiedCheckpoint, VerifiedCheckpointContents, VersionedFullCheckpointContents,
65    },
66    storage::WriteStore,
67};
68use tap::Pipe;
69use tokio::sync::oneshot;
70use tokio::{
71    sync::{broadcast, mpsc, watch},
72    task::{AbortHandle, JoinSet},
73};
74use tracing::{debug, info, instrument, trace, warn};
75
76#[allow(clippy::result_large_err)]
77mod generated {
78    include!(concat!(env!("OUT_DIR"), "/sui.StateSync.rs"));
79}
80mod builder;
81mod metrics;
82mod server;
83#[cfg(test)]
84mod tests;
85mod worker;
86
87use self::{metrics::Metrics, server::CheckpointContentsDownloadLimitLayer};
88use crate::state_sync::worker::process_archive_checkpoint;
89pub use builder::{Builder, UnstartedStateSync};
90pub use generated::{
91    state_sync_client::StateSyncClient,
92    state_sync_server::{StateSync, StateSyncServer},
93};
94pub use server::GetCheckpointAvailabilityResponse;
95pub use server::GetCheckpointSummaryRequest;
96use sui_config::node::ArchiveReaderConfig;
97use sui_storage::object_store::util::{build_object_store, fetch_checkpoint};
98use sui_storage::verify_checkpoint;
99
100/// A handle to the StateSync subsystem.
101///
102/// This handle can be cloned and shared. Once all copies of a StateSync system's Handle have been
103/// dropped, the StateSync system will be gracefully shutdown.
104#[derive(Clone, Debug)]
105pub struct Handle {
106    sender: mpsc::Sender<StateSyncMessage>,
107    checkpoint_event_sender: broadcast::Sender<VerifiedCheckpoint>,
108    metrics: Metrics,
109}
110
111impl Handle {
112    /// Send a newly minted checkpoint from Consensus to StateSync so that it can be disseminated
113    /// to other nodes on the network.
114    ///
115    /// # Invariant
116    ///
117    /// Consensus must only notify StateSync of new checkpoints that have been fully committed to
118    /// persistent storage. This includes CheckpointContents and all Transactions and
119    /// TransactionEffects included therein.
120    pub async fn send_checkpoint(&self, checkpoint: VerifiedCheckpoint) {
121        self.sender
122            .send(StateSyncMessage::VerifiedCheckpoint(Box::new(checkpoint)))
123            .await
124            .unwrap()
125    }
126
127    /// Subscribe to the stream of checkpoints that have been fully synchronized and downloaded.
128    pub fn subscribe_to_synced_checkpoints(&self) -> broadcast::Receiver<VerifiedCheckpoint> {
129        self.checkpoint_event_sender.subscribe()
130    }
131
132    /// Returns the number of peers reported due to consistent state sync failures.
133    pub fn get_peers_reported_for_failure(&self) -> u64 {
134        self.metrics.get_peers_reported_for_failure()
135    }
136}
137
138pub(super) fn compute_adaptive_timeout(
139    tx_count: u64,
140    min_timeout: Duration,
141    max_timeout: Duration,
142) -> Duration {
143    const MAX_TRANSACTIONS_PER_CHECKPOINT: u64 = 10_000;
144    const JITTER_FRACTION: f64 = 0.1;
145
146    let max_timeout = max_timeout.max(min_timeout);
147    let ratio = (tx_count as f64 / MAX_TRANSACTIONS_PER_CHECKPOINT as f64).min(1.0);
148    let extra = Duration::from_secs_f64((max_timeout - min_timeout).as_secs_f64() * ratio);
149    let base = min_timeout + extra;
150
151    let jitter_range = base.as_secs_f64() * JITTER_FRACTION;
152    let jitter = rand::thread_rng().gen_range(-jitter_range..jitter_range);
153    Duration::from_secs_f64((base.as_secs_f64() + jitter).max(min_timeout.as_secs_f64()))
154}
155
156#[cfg_attr(test, derive(Debug))]
157pub(super) struct PeerScore {
158    successes: VecDeque<(Instant, u64 /* response_size_bytes */, Duration)>,
159    failures: VecDeque<Instant>,
160    window: Duration,
161    failure_rate: f64,
162    failing_since: Option<Instant>,
163}
164
165impl PeerScore {
166    const MAX_SAMPLES: usize = 20;
167    const MIN_SAMPLES_FOR_FAILURE: usize = 10;
168
169    pub(super) fn new(window: Duration, failure_rate: f64) -> Self {
170        Self {
171            successes: VecDeque::new(),
172            failures: VecDeque::new(),
173            window,
174            failure_rate,
175            failing_since: None,
176        }
177    }
178
179    pub(super) fn record_success(&mut self, size: u64, response_time: Duration) {
180        let now = Instant::now();
181        self.successes.push_back((now, size, response_time));
182        while self.successes.len() > Self::MAX_SAMPLES {
183            self.successes.pop_front();
184        }
185        // Note: a single success does NOT clear `failing_since`. Whether the peer is
186        // "consistently failing" is driven by the windowed failure RATE (see
187        // `update_failing_state`), so an occasional success while the rate stays high (e.g.
188        // a brief reconnect during connection churn) must not keep resetting the clock and
189        // prevent the peer from ever being reported.
190    }
191
192    pub(super) fn record_failure(&mut self) {
193        let now = Instant::now();
194        self.failures.push_back(now);
195        while self.failures.len() > Self::MAX_SAMPLES {
196            self.failures.pop_front();
197        }
198    }
199
200    pub(super) fn is_failing(&self) -> bool {
201        let now = Instant::now();
202        let recent_failures = self
203            .failures
204            .iter()
205            .filter(|ts| now.duration_since(**ts) < self.window)
206            .count();
207        let recent_successes = self
208            .successes
209            .iter()
210            .filter(|(ts, _, _)| now.duration_since(*ts) < self.window)
211            .count();
212
213        let total = recent_failures + recent_successes;
214        if total < Self::MIN_SAMPLES_FOR_FAILURE {
215            return false;
216        }
217
218        let rate = recent_failures as f64 / total as f64;
219        rate >= self.failure_rate
220    }
221
222    pub(super) fn update_failing_state(&mut self) {
223        if self.is_failing() {
224            // Start the consistently-failing clock when the windowed failure rate first
225            // crosses the threshold, and let it accumulate while the rate stays high.
226            if self.failing_since.is_none() {
227                self.failing_since = Some(Instant::now());
228            }
229        } else {
230            // The windowed failure rate dropped below the threshold: the peer is no longer
231            // failing, so clear the clock. Only an actual drop in the rate resets it.
232            self.failing_since = None;
233        }
234    }
235
236    pub(super) fn reset_failing_since(&mut self) {
237        self.failing_since = None;
238    }
239
240    pub(super) fn consistently_failing(&self, threshold: Duration) -> bool {
241        match self.failing_since {
242            Some(since) => since.elapsed() >= threshold,
243            None => false,
244        }
245    }
246
247    pub(super) fn effective_throughput(&self) -> Option<f64> {
248        let now = Instant::now();
249        let (total_size, total_time) = self
250            .successes
251            .iter()
252            .filter(|(ts, _, _)| now.duration_since(*ts) < self.window)
253            .fold((0u64, Duration::ZERO), |(size, time), (_, s, d)| {
254                (size + s, time + *d)
255            });
256
257        if total_size == 0 {
258            return None;
259        }
260
261        if total_time.is_zero() {
262            return None;
263        }
264
265        Some(total_size as f64 / total_time.as_secs_f64())
266    }
267}
268
269struct PeerHeights {
270    /// Table used to track the highest checkpoint for each of our peers.
271    peers: HashMap<PeerId, PeerStateSyncInfo>,
272    unprocessed_checkpoints: HashMap<CheckpointDigest, Checkpoint>,
273    sequence_number_to_digest: HashMap<CheckpointSequenceNumber, CheckpointDigest>,
274    scores: HashMap<PeerId, PeerScore>,
275
276    wait_interval_when_no_peer_to_sync_content: Duration,
277    peer_scoring_window: Duration,
278    peer_failure_rate: f64,
279    checkpoint_content_timeout_min: Duration,
280    checkpoint_content_timeout_max: Duration,
281    exploration_probability: f64,
282}
283
284#[derive(Copy, Clone, Debug, PartialEq, Eq)]
285struct PeerStateSyncInfo {
286    /// The digest of the Peer's genesis checkpoint.
287    genesis_checkpoint_digest: CheckpointDigest,
288    /// Indicates if this Peer is on the same chain as us.
289    on_same_chain_as_us: bool,
290    /// Highest checkpoint sequence number we know of for this Peer.
291    height: CheckpointSequenceNumber,
292    /// lowest available checkpoint watermark for this Peer.
293    /// This defaults to 0 for now.
294    lowest: CheckpointSequenceNumber,
295}
296
297impl PeerHeights {
298    pub fn highest_known_checkpoint_sequence_number(&self) -> Option<CheckpointSequenceNumber> {
299        self.peers
300            .values()
301            .filter_map(|info| info.on_same_chain_as_us.then_some(info.height))
302            .max()
303    }
304
305    pub fn peers_on_same_chain(&self) -> impl Iterator<Item = (&PeerId, &PeerStateSyncInfo)> {
306        self.peers
307            .iter()
308            .filter(|(_peer_id, info)| info.on_same_chain_as_us)
309    }
310
311    // Returns a bool that indicates if the update was done successfully.
312    //
313    // This will return false if the given peer doesn't have an entry or is not on the same chain
314    // as us
315    #[instrument(level = "debug", skip_all, fields(peer_id=?peer_id, checkpoint=?checkpoint.sequence_number()))]
316    pub fn update_peer_info(
317        &mut self,
318        peer_id: PeerId,
319        checkpoint: Checkpoint,
320        low_watermark: Option<CheckpointSequenceNumber>,
321    ) -> bool {
322        debug!("Update peer info");
323
324        let info = match self.peers.get_mut(&peer_id) {
325            Some(info) if info.on_same_chain_as_us => info,
326            _ => return false,
327        };
328
329        info.height = std::cmp::max(*checkpoint.sequence_number(), info.height);
330        if let Some(low_watermark) = low_watermark {
331            info.lowest = low_watermark;
332        }
333        self.insert_checkpoint(checkpoint);
334
335        true
336    }
337
338    #[instrument(level = "debug", skip_all, fields(peer_id=?peer_id, lowest = ?info.lowest, height = ?info.height))]
339    pub fn insert_peer_info(&mut self, peer_id: PeerId, info: PeerStateSyncInfo) {
340        use std::collections::hash_map::Entry;
341        debug!("Insert peer info");
342
343        match self.peers.entry(peer_id) {
344            Entry::Occupied(mut entry) => {
345                // If there's already an entry and the genesis checkpoint digests match then update
346                // the maximum height. Otherwise we'll use the more recent one
347                let entry = entry.get_mut();
348                if entry.genesis_checkpoint_digest == info.genesis_checkpoint_digest {
349                    entry.height = std::cmp::max(entry.height, info.height);
350                } else {
351                    *entry = info;
352                }
353            }
354            Entry::Vacant(entry) => {
355                entry.insert(info);
356            }
357        }
358    }
359
360    pub fn mark_peer_as_not_on_same_chain(&mut self, peer_id: PeerId) {
361        if let Some(info) = self.peers.get_mut(&peer_id) {
362            info.on_same_chain_as_us = false;
363        }
364        self.scores.remove(&peer_id);
365    }
366
367    /// Updates the peer's height without storing any checkpoint data.
368    /// Returns false if the peer doesn't exist or is not on the same chain.
369    pub fn update_peer_height(
370        &mut self,
371        peer_id: PeerId,
372        height: CheckpointSequenceNumber,
373        low_watermark: Option<CheckpointSequenceNumber>,
374    ) -> bool {
375        let info = match self.peers.get_mut(&peer_id) {
376            Some(info) if info.on_same_chain_as_us => info,
377            _ => return false,
378        };
379
380        info.height = std::cmp::max(height, info.height);
381        if let Some(low_watermark) = low_watermark {
382            info.lowest = low_watermark;
383        }
384
385        true
386    }
387
388    pub fn cleanup_old_checkpoints(&mut self, sequence_number: CheckpointSequenceNumber) {
389        self.unprocessed_checkpoints
390            .retain(|_digest, checkpoint| *checkpoint.sequence_number() > sequence_number);
391        self.sequence_number_to_digest
392            .retain(|&s, _digest| s > sequence_number);
393    }
394
395    /// Inserts a checkpoint into the unprocessed checkpoints store.
396    /// Only one checkpoint per sequence number is stored. If a checkpoint with the same
397    /// sequence number but different digest already exists, the new one is dropped.
398    // TODO: also record who gives this checkpoint info for peer quality measurement?
399    pub fn insert_checkpoint(&mut self, checkpoint: Checkpoint) {
400        let digest = *checkpoint.digest();
401        let sequence_number = *checkpoint.sequence_number();
402
403        // Check if we already have a checkpoint for this sequence number.
404        if let Some(existing_digest) = self.sequence_number_to_digest.get(&sequence_number) {
405            if *existing_digest == digest {
406                // Same checkpoint, nothing to do.
407                return;
408            }
409            tracing::info!(
410                ?sequence_number,
411                ?existing_digest,
412                ?digest,
413                "received checkpoint with same sequence number but different digest, dropping new checkpoint"
414            );
415            return;
416        }
417
418        self.unprocessed_checkpoints.insert(digest, checkpoint);
419        self.sequence_number_to_digest
420            .insert(sequence_number, digest);
421    }
422
423    pub fn remove_checkpoint(&mut self, digest: &CheckpointDigest) {
424        if let Some(checkpoint) = self.unprocessed_checkpoints.remove(digest) {
425            self.sequence_number_to_digest
426                .remove(checkpoint.sequence_number());
427        }
428    }
429
430    pub fn get_checkpoint_by_sequence_number(
431        &self,
432        sequence_number: CheckpointSequenceNumber,
433    ) -> Option<&Checkpoint> {
434        self.sequence_number_to_digest
435            .get(&sequence_number)
436            .and_then(|digest| self.get_checkpoint_by_digest(digest))
437    }
438
439    pub fn get_checkpoint_by_digest(&self, digest: &CheckpointDigest) -> Option<&Checkpoint> {
440        self.unprocessed_checkpoints.get(digest)
441    }
442
443    #[cfg(test)]
444    pub fn set_wait_interval_when_no_peer_to_sync_content(&mut self, duration: Duration) {
445        self.wait_interval_when_no_peer_to_sync_content = duration;
446    }
447
448    pub fn wait_interval_when_no_peer_to_sync_content(&self) -> Duration {
449        self.wait_interval_when_no_peer_to_sync_content
450    }
451
452    pub fn record_success(&mut self, peer_id: PeerId, size: u64, response_time: Duration) {
453        if !self.peers.contains_key(&peer_id) {
454            return;
455        }
456        self.scores
457            .entry(peer_id)
458            .or_insert_with(|| PeerScore::new(self.peer_scoring_window, self.peer_failure_rate))
459            .record_success(size, response_time);
460    }
461
462    pub fn record_failure(&mut self, peer_id: PeerId) {
463        if !self.peers.contains_key(&peer_id) {
464            return;
465        }
466        self.scores
467            .entry(peer_id)
468            .or_insert_with(|| PeerScore::new(self.peer_scoring_window, self.peer_failure_rate))
469            .record_failure();
470    }
471
472    pub fn get_throughput(&self, peer_id: &PeerId) -> Option<f64> {
473        self.scores
474            .get(peer_id)
475            .and_then(|s| s.effective_throughput())
476    }
477
478    pub fn is_failing(&self, peer_id: &PeerId) -> bool {
479        self.scores
480            .get(peer_id)
481            .map(|s| s.is_failing())
482            .unwrap_or(false)
483    }
484
485    pub fn update_failing_states(&mut self) {
486        for score in self.scores.values_mut() {
487            score.update_failing_state();
488        }
489    }
490
491    pub fn find_peer_to_report_for_failure(&self, threshold: Duration) -> Option<PeerId> {
492        self.scores
493            .iter()
494            .filter(|(peer_id, score)| {
495                score.consistently_failing(threshold)
496                    && self
497                        .peers
498                        .get(peer_id)
499                        .is_some_and(|info| info.on_same_chain_as_us)
500            })
501            .max_by_key(|(_, score)| {
502                score
503                    .failing_since
504                    .map(|since| since.elapsed())
505                    .unwrap_or(Duration::ZERO)
506            })
507            .map(|(peer_id, _)| *peer_id)
508    }
509}
510
511// PeerBalancer selects peers using weighted random selection:
512// - Most of the time: select from known peers, weighted by throughput
513// - With configured probability: select from unknown peers (to explore)
514// - Failing peers: only as last resort
515#[derive(Clone)]
516struct PeerBalancer {
517    known_peers: Vec<(anemo::Peer, PeerStateSyncInfo, f64)>,
518    unknown_peers: Vec<(anemo::Peer, PeerStateSyncInfo, f64)>,
519    failing_peers: Vec<(anemo::Peer, PeerStateSyncInfo)>,
520    requested_checkpoint: Option<CheckpointSequenceNumber>,
521    request_type: PeerCheckpointRequestType,
522    exploration_probability: f64,
523}
524
525#[derive(Clone)]
526enum PeerCheckpointRequestType {
527    Summary,
528    Content,
529}
530
531impl PeerBalancer {
532    pub fn new(
533        network: &anemo::Network,
534        peer_heights: Arc<RwLock<PeerHeights>>,
535        request_type: PeerCheckpointRequestType,
536    ) -> Self {
537        let peer_heights_guard = peer_heights.read().unwrap();
538
539        let mut known_peers = Vec::new();
540        let mut unknown_peers = Vec::new();
541        let mut failing_peers = Vec::new();
542        let exploration_probability = peer_heights_guard.exploration_probability;
543
544        for (peer_id, info) in peer_heights_guard.peers_on_same_chain() {
545            let Some(peer) = network.peer(*peer_id) else {
546                continue;
547            };
548            let rtt_secs = peer.connection_rtt().as_secs_f64();
549
550            if peer_heights_guard.is_failing(peer_id) {
551                failing_peers.push((peer, *info));
552            } else if let Some(throughput) = peer_heights_guard.get_throughput(peer_id) {
553                known_peers.push((peer, *info, throughput));
554            } else {
555                unknown_peers.push((peer, *info, rtt_secs));
556            }
557        }
558        drop(peer_heights_guard);
559
560        unknown_peers.sort_by(|(_, _, rtt_a), (_, _, rtt_b)| {
561            rtt_a
562                .partial_cmp(rtt_b)
563                .unwrap_or(std::cmp::Ordering::Equal)
564        });
565
566        Self {
567            known_peers,
568            unknown_peers,
569            failing_peers,
570            requested_checkpoint: None,
571            request_type,
572            exploration_probability,
573        }
574    }
575
576    pub fn with_checkpoint(mut self, checkpoint: CheckpointSequenceNumber) -> Self {
577        self.requested_checkpoint = Some(checkpoint);
578        self
579    }
580
581    fn is_eligible(&self, info: &PeerStateSyncInfo) -> bool {
582        let requested = self.requested_checkpoint.unwrap_or(0);
583        match &self.request_type {
584            PeerCheckpointRequestType::Summary => info.height >= requested,
585            PeerCheckpointRequestType::Content => {
586                info.height >= requested && info.lowest <= requested
587            }
588        }
589    }
590
591    fn select_by_throughput(&mut self) -> Option<(anemo::Peer, PeerStateSyncInfo)> {
592        let eligible: Vec<_> = self
593            .known_peers
594            .iter()
595            .enumerate()
596            .filter(|(_, (_, info, _))| self.is_eligible(info))
597            .map(|(i, (_, _, t))| (i, *t))
598            .collect();
599
600        if eligible.is_empty() {
601            return None;
602        }
603
604        let total: f64 = eligible.iter().map(|(_, t)| t).sum();
605        let mut pick = rand::thread_rng().gen_range(0.0..total);
606
607        for (idx, throughput) in &eligible {
608            pick -= throughput;
609            if pick <= 0.0 {
610                let (peer, info, _) = self.known_peers.remove(*idx);
611                return Some((peer, info));
612            }
613        }
614
615        let (idx, _) = eligible.last().unwrap();
616        let (peer, info, _) = self.known_peers.remove(*idx);
617        Some((peer, info))
618    }
619
620    fn select_by_rtt(&mut self) -> Option<(anemo::Peer, PeerStateSyncInfo)> {
621        let pos = self
622            .unknown_peers
623            .iter()
624            .position(|(_, info, _)| self.is_eligible(info))?;
625        let (peer, info, _) = self.unknown_peers.remove(pos);
626        Some((peer, info))
627    }
628
629    fn select_failing(&mut self) -> Option<(anemo::Peer, PeerStateSyncInfo)> {
630        let pos = self
631            .failing_peers
632            .iter()
633            .position(|(_, info)| self.is_eligible(info))?;
634        Some(self.failing_peers.remove(pos))
635    }
636}
637
638impl Iterator for PeerBalancer {
639    type Item = StateSyncClient<anemo::Peer>;
640
641    fn next(&mut self) -> Option<Self::Item> {
642        let has_eligible_known = self
643            .known_peers
644            .iter()
645            .any(|(_, info, _)| self.is_eligible(info));
646        let has_eligible_unknown = self
647            .unknown_peers
648            .iter()
649            .any(|(_, info, _)| self.is_eligible(info));
650
651        let explore = has_eligible_unknown
652            && (!has_eligible_known || rand::thread_rng().gen_bool(self.exploration_probability));
653
654        if explore && let Some((peer, _)) = self.select_by_rtt() {
655            return Some(StateSyncClient::new(peer));
656        }
657
658        if has_eligible_known && let Some((peer, _)) = self.select_by_throughput() {
659            return Some(StateSyncClient::new(peer));
660        }
661
662        if let Some((peer, _)) = self.select_failing() {
663            return Some(StateSyncClient::new(peer));
664        }
665
666        None
667    }
668}
669
670#[derive(Clone, Debug)]
671enum StateSyncMessage {
672    StartSyncJob,
673    // Validators will send this to the StateSyncEventLoop in order to kick off notifying our peers
674    // of the new checkpoint.
675    VerifiedCheckpoint(Box<VerifiedCheckpoint>),
676    // Notification that the checkpoint content sync task will send to the event loop in the event
677    // it was able to successfully sync a checkpoint's contents. If multiple checkpoints were
678    // synced at the same time, only the highest checkpoint is sent.
679    SyncedCheckpoint(Box<VerifiedCheckpoint>),
680}
681
682struct StateSyncEventLoop<S> {
683    config: StateSyncConfig,
684
685    mailbox: mpsc::Receiver<StateSyncMessage>,
686    /// Weak reference to our own mailbox
687    weak_sender: mpsc::WeakSender<StateSyncMessage>,
688
689    tasks: JoinSet<()>,
690    sync_checkpoint_summaries_task: Option<AbortHandle>,
691    sync_checkpoint_contents_task: Option<AbortHandle>,
692    download_limit_layer: Option<CheckpointContentsDownloadLimitLayer>,
693
694    store: S,
695    peer_heights: Arc<RwLock<PeerHeights>>,
696    checkpoint_event_sender: broadcast::Sender<VerifiedCheckpoint>,
697    network: anemo::Network,
698    metrics: Metrics,
699
700    sync_checkpoint_from_archive_task: Option<AbortHandle>,
701    archive_config: Option<ArchiveReaderConfig>,
702    discovery_sender: Option<crate::discovery::Sender>,
703}
704
705impl<S> StateSyncEventLoop<S>
706where
707    S: WriteStore + Clone + Send + Sync + 'static,
708{
709    // Note: A great deal of care is taken to ensure that all event handlers are non-asynchronous
710    // and that the only "await" points are from the select macro picking which event to handle.
711    // This ensures that the event loop is able to process events at a high speed and reduce the
712    // chance for building up a backlog of events to process.
713    pub async fn start(mut self) {
714        info!("State-Synchronizer started");
715
716        self.config.pinned_checkpoints.sort();
717
718        let mut interval = tokio::time::interval(self.config.interval_period());
719        let mut peer_events = {
720            let (subscriber, peers) = self.network.subscribe().unwrap();
721            for peer_id in peers {
722                self.spawn_get_latest_from_peer(peer_id);
723            }
724            subscriber
725        };
726        let (
727            target_checkpoint_contents_sequence_sender,
728            target_checkpoint_contents_sequence_receiver,
729        ) = watch::channel(0);
730
731        // Spawn tokio task to update metrics periodically in the background
732        let (_sender, receiver) = oneshot::channel();
733        tokio::spawn(update_checkpoint_watermark_metrics(
734            receiver,
735            self.store.clone(),
736            self.metrics.clone(),
737        ));
738
739        // Start checkpoint contents sync loop.
740        let task = sync_checkpoint_contents(
741            self.network.clone(),
742            self.store.clone(),
743            self.peer_heights.clone(),
744            self.weak_sender.clone(),
745            self.checkpoint_event_sender.clone(),
746            self.config.checkpoint_content_download_concurrency(),
747            self.config.checkpoint_content_download_tx_concurrency(),
748            self.config.use_get_checkpoint_contents_v2(),
749            target_checkpoint_contents_sequence_receiver,
750        );
751        let task_handle = self.tasks.spawn(task);
752        self.sync_checkpoint_contents_task = Some(task_handle);
753
754        // Start archive based checkpoint content sync loop.
755        // TODO: Consider switching to sync from archive only on startup.
756        // Right now because the peer set is fixed at startup, a node may eventually
757        // end up with peers who have all purged their local state. In such a scenario it will be
758        // stuck until restart when it ends up with a different set of peers. Once the discovery
759        // mechanism can dynamically identify and connect to other peers on the network, we will rely
760        // on sync from archive as a fall back.
761        let task = sync_checkpoint_contents_from_archive(
762            self.network.clone(),
763            self.archive_config.clone(),
764            self.store.clone(),
765            self.peer_heights.clone(),
766            self.metrics.clone(),
767        );
768        let task_handle = self.tasks.spawn(task);
769        self.sync_checkpoint_from_archive_task = Some(task_handle);
770
771        // Start main loop.
772        loop {
773            tokio::select! {
774                now = interval.tick() => {
775                    self.handle_tick(now.into_std());
776                },
777                maybe_message = self.mailbox.recv() => {
778                    // Once all handles to our mailbox have been dropped this
779                    // will yield `None` and we can terminate the event loop
780                    if let Some(message) = maybe_message {
781                        self.handle_message(message);
782                    } else {
783                        break;
784                    }
785                },
786                peer_event = peer_events.recv() => {
787                    self.handle_peer_event(peer_event);
788                },
789                Some(task_result) = self.tasks.join_next() => {
790                    match task_result {
791                        Ok(()) => {},
792                        Err(e) => {
793                            if e.is_cancelled() {
794                                // avoid crashing on ungraceful shutdown
795                            } else if e.is_panic() {
796                                // propagate panics.
797                                std::panic::resume_unwind(e.into_panic());
798                            } else {
799                                panic!("task failed: {e}");
800                            }
801                        },
802                    };
803
804                    if matches!(&self.sync_checkpoint_contents_task, Some(t) if t.is_finished()) {
805                        panic!("sync_checkpoint_contents task unexpectedly terminated")
806                    }
807
808                    if matches!(&self.sync_checkpoint_summaries_task, Some(t) if t.is_finished()) {
809                        self.sync_checkpoint_summaries_task = None;
810                    }
811
812                    if matches!(&self.sync_checkpoint_from_archive_task, Some(t) if t.is_finished()) {
813                        panic!("sync_checkpoint_from_archive task unexpectedly terminated")
814                    }
815                },
816            }
817
818            self.maybe_start_checkpoint_summary_sync_task();
819            self.maybe_trigger_checkpoint_contents_sync_task(
820                &target_checkpoint_contents_sequence_sender,
821            );
822        }
823
824        info!("State-Synchronizer ended");
825    }
826
827    fn handle_message(&mut self, message: StateSyncMessage) {
828        debug!("Received message: {:?}", message);
829        match message {
830            StateSyncMessage::StartSyncJob => self.maybe_start_checkpoint_summary_sync_task(),
831            StateSyncMessage::VerifiedCheckpoint(checkpoint) => {
832                self.handle_checkpoint_from_consensus(checkpoint)
833            }
834            // After we've successfully synced a checkpoint we can notify our peers
835            StateSyncMessage::SyncedCheckpoint(checkpoint) => {
836                self.spawn_notify_peers_of_checkpoint(*checkpoint)
837            }
838        }
839    }
840
841    // Handle a checkpoint that we received from consensus
842    #[instrument(level = "debug", skip_all)]
843    fn handle_checkpoint_from_consensus(&mut self, checkpoint: Box<VerifiedCheckpoint>) {
844        // Always check previous_digest matches in case there is a gap between
845        // state sync and consensus.
846        let prev_digest = *self.store.get_checkpoint_by_sequence_number(checkpoint.sequence_number() - 1)
847            .unwrap_or_else(|| panic!("Got checkpoint {} from consensus but cannot find checkpoint {} in certified_checkpoints", checkpoint.sequence_number(), checkpoint.sequence_number() - 1))
848            .digest();
849        if checkpoint.previous_digest != Some(prev_digest) {
850            panic!(
851                "Checkpoint {} from consensus has mismatched previous_digest, expected: {:?}, actual: {:?}",
852                checkpoint.sequence_number(),
853                Some(prev_digest),
854                checkpoint.previous_digest
855            );
856        }
857
858        let latest_checkpoint = self
859            .store
860            .get_highest_verified_checkpoint()
861            .expect("store operation should not fail");
862
863        // If this is an older checkpoint, just ignore it
864        if latest_checkpoint.sequence_number() >= checkpoint.sequence_number() {
865            return;
866        }
867
868        let checkpoint = *checkpoint;
869        let next_sequence_number = latest_checkpoint.sequence_number().checked_add(1).unwrap();
870        if *checkpoint.sequence_number() > next_sequence_number {
871            debug!(
872                "consensus sent too new of a checkpoint, expecting: {}, got: {}",
873                next_sequence_number,
874                checkpoint.sequence_number()
875            );
876        }
877
878        // Because checkpoint from consensus sends in order, when we have checkpoint n,
879        // we must have all of the checkpoints before n from either state sync or consensus.
880        #[cfg(debug_assertions)]
881        {
882            let _ = (next_sequence_number..=*checkpoint.sequence_number())
883                .map(|n| {
884                    let checkpoint = self
885                        .store
886                        .get_checkpoint_by_sequence_number(n)
887                        .unwrap_or_else(|| panic!("store should contain checkpoint {n}"));
888                    self.store
889                        .get_full_checkpoint_contents(Some(n), &checkpoint.content_digest)
890                        .unwrap_or_else(|| {
891                            panic!(
892                                "store should contain checkpoint contents for {:?}",
893                                checkpoint.content_digest
894                            )
895                        });
896                })
897                .collect::<Vec<_>>();
898        }
899
900        // If this is the last checkpoint of a epoch, we need to make sure
901        // new committee is in store before we verify newer checkpoints in next epoch.
902        // This could happen before this validator's reconfiguration finishes, because
903        // state sync does not reconfig.
904        // TODO: make CheckpointAggregator use WriteStore so we don't need to do this
905        // committee insertion in two places (only in `insert_checkpoint`).
906        if let Some(EndOfEpochData {
907            next_epoch_committee,
908            ..
909        }) = checkpoint.end_of_epoch_data.as_ref()
910        {
911            let next_committee = next_epoch_committee.iter().cloned().collect();
912            let committee =
913                Committee::new(checkpoint.epoch().checked_add(1).unwrap(), next_committee);
914            self.store
915                .insert_committee(committee)
916                .expect("store operation should not fail");
917        }
918
919        self.store
920            .update_highest_verified_checkpoint(&checkpoint)
921            .expect("store operation should not fail");
922        self.store
923            .update_highest_synced_checkpoint(&checkpoint)
924            .expect("store operation should not fail");
925
926        // We don't care if no one is listening as this is a broadcast channel
927        let _ = self.checkpoint_event_sender.send(checkpoint.clone());
928
929        self.spawn_notify_peers_of_checkpoint(checkpoint);
930    }
931
932    fn handle_peer_event(
933        &mut self,
934        peer_event: Result<PeerEvent, tokio::sync::broadcast::error::RecvError>,
935    ) {
936        use tokio::sync::broadcast::error::RecvError;
937
938        match peer_event {
939            Ok(PeerEvent::NewPeer(peer_id)) => {
940                self.spawn_get_latest_from_peer(peer_id);
941            }
942            Ok(PeerEvent::LostPeer(peer_id, _)) => {
943                let mut heights = self.peer_heights.write().unwrap();
944                heights.peers.remove(&peer_id);
945                heights.scores.remove(&peer_id);
946            }
947
948            Err(RecvError::Closed) => {
949                panic!("PeerEvent channel shouldn't be able to be closed");
950            }
951
952            Err(RecvError::Lagged(_)) => {
953                trace!("State-Sync fell behind processing PeerEvents");
954            }
955        }
956    }
957
958    fn spawn_get_latest_from_peer(&mut self, peer_id: PeerId) {
959        if let Some(peer) = self.network.peer(peer_id) {
960            let genesis_checkpoint_digest = *self
961                .store
962                .get_checkpoint_by_sequence_number(0)
963                .expect("store should contain genesis checkpoint")
964                .digest();
965            let task = get_latest_from_peer(
966                genesis_checkpoint_digest,
967                peer,
968                self.peer_heights.clone(),
969                self.config.timeout(),
970            );
971            self.tasks.spawn(task);
972        }
973    }
974
975    fn handle_tick(&mut self, _now: std::time::Instant) {
976        let task = query_peers_for_their_latest_checkpoint(
977            self.network.clone(),
978            self.peer_heights.clone(),
979            self.weak_sender.clone(),
980            self.config.timeout(),
981        );
982        self.tasks.spawn(task);
983
984        if let Some(layer) = self.download_limit_layer.as_ref() {
985            layer.maybe_prune_map();
986        }
987
988        self.maybe_report_failing_peer();
989    }
990
991    fn maybe_report_failing_peer(&mut self) {
992        let threshold = self.config.peer_disconnect_threshold();
993        if threshold.is_zero() {
994            return;
995        }
996
997        let mut peer_heights = self.peer_heights.write().unwrap();
998        peer_heights.update_failing_states();
999
1000        let Some(peer_id) = peer_heights.find_peer_to_report_for_failure(threshold) else {
1001            return;
1002        };
1003        // Reset the failing clock so this peer isn't re-reported on the next tick.
1004        // If the peer is still failing, update_failing_states will restart the clock
1005        // and the peer must fail for another full threshold before being reported again.
1006        // Discovery may decline to disconnect (e.g. too few peers), so this also
1007        // prevents spamming reports every tick in that case.
1008        if let Some(score) = peer_heights.scores.get_mut(&peer_id) {
1009            score.reset_failing_since();
1010        }
1011        drop(peer_heights);
1012
1013        info!("reporting peer {peer_id} for consistent state sync failures");
1014        if let Some(sender) = &self.discovery_sender {
1015            sender.report_peer_failure(peer_id);
1016        } else {
1017            let _ = self.network.disconnect(peer_id);
1018        }
1019        self.metrics.inc_peers_reported_for_failure();
1020    }
1021
1022    fn maybe_start_checkpoint_summary_sync_task(&mut self) {
1023        // Only run one sync task at a time
1024        if self.sync_checkpoint_summaries_task.is_some() {
1025            return;
1026        }
1027
1028        let highest_processed_checkpoint = self
1029            .store
1030            .get_highest_verified_checkpoint()
1031            .expect("store operation should not fail");
1032
1033        let highest_known_sequence_number = self
1034            .peer_heights
1035            .read()
1036            .unwrap()
1037            .highest_known_checkpoint_sequence_number();
1038
1039        if let Some(target_seq) = highest_known_sequence_number
1040            && *highest_processed_checkpoint.sequence_number() < target_seq
1041        {
1042            // Limit the per-sync batch size according to config.
1043            let max_batch_size = self.config.max_checkpoint_sync_batch_size();
1044            let limited_target = std::cmp::min(
1045                target_seq,
1046                highest_processed_checkpoint
1047                    .sequence_number()
1048                    .saturating_add(max_batch_size),
1049            );
1050            let was_limited = limited_target < target_seq;
1051
1052            // Start sync job.
1053            let weak_sender = self.weak_sender.clone();
1054            let task = sync_to_checkpoint(
1055                self.network.clone(),
1056                self.store.clone(),
1057                self.peer_heights.clone(),
1058                self.metrics.clone(),
1059                self.config.pinned_checkpoints.clone(),
1060                self.config.checkpoint_header_download_concurrency(),
1061                self.config.timeout(),
1062                limited_target,
1063            )
1064            .map(move |result| match result {
1065                Ok(()) => {
1066                    // If we limited the sync range, immediately trigger
1067                    // another sync to continue catching up.
1068                    if was_limited && let Some(sender) = weak_sender.upgrade() {
1069                        let _ = sender.try_send(StateSyncMessage::StartSyncJob);
1070                    }
1071                }
1072                Err(e) => {
1073                    debug!("error syncing checkpoint {e}");
1074                }
1075            });
1076            let task_handle = self.tasks.spawn(task);
1077            self.sync_checkpoint_summaries_task = Some(task_handle);
1078        }
1079    }
1080
1081    fn maybe_trigger_checkpoint_contents_sync_task(
1082        &mut self,
1083        target_sequence_channel: &watch::Sender<CheckpointSequenceNumber>,
1084    ) {
1085        let highest_verified_checkpoint = self
1086            .store
1087            .get_highest_verified_checkpoint()
1088            .expect("store operation should not fail");
1089        let highest_synced_checkpoint = self
1090            .store
1091            .get_highest_synced_checkpoint()
1092            .expect("store operation should not fail");
1093
1094        if highest_verified_checkpoint.sequence_number()
1095            > highest_synced_checkpoint.sequence_number()
1096            // skip if we aren't connected to any peers that can help
1097            && self
1098                .peer_heights
1099                .read()
1100                .unwrap()
1101                .highest_known_checkpoint_sequence_number()
1102                > Some(*highest_synced_checkpoint.sequence_number())
1103        {
1104            let _ = target_sequence_channel.send_if_modified(|num| {
1105                let new_num = *highest_verified_checkpoint.sequence_number();
1106                if *num == new_num {
1107                    return false;
1108                }
1109                *num = new_num;
1110                true
1111            });
1112        }
1113    }
1114
1115    fn spawn_notify_peers_of_checkpoint(&mut self, checkpoint: VerifiedCheckpoint) {
1116        let task = notify_peers_of_checkpoint(
1117            self.network.clone(),
1118            self.peer_heights.clone(),
1119            checkpoint,
1120            self.config.timeout(),
1121        );
1122        self.tasks.spawn(task);
1123    }
1124}
1125
1126async fn notify_peers_of_checkpoint(
1127    network: anemo::Network,
1128    peer_heights: Arc<RwLock<PeerHeights>>,
1129    checkpoint: VerifiedCheckpoint,
1130    timeout: Duration,
1131) {
1132    let futs = peer_heights
1133        .read()
1134        .unwrap()
1135        .peers_on_same_chain()
1136        // Filter out any peers who we know already have a checkpoint higher than this one
1137        .filter_map(|(peer_id, info)| {
1138            (*checkpoint.sequence_number() > info.height).then_some(peer_id)
1139        })
1140        // Filter out any peers who we aren't connected with
1141        .flat_map(|peer_id| network.peer(*peer_id))
1142        .map(StateSyncClient::new)
1143        .map(|mut client| {
1144            let request = Request::new(checkpoint.inner().clone()).with_timeout(timeout);
1145            async move { client.push_checkpoint_summary(request).await }
1146        })
1147        .collect::<Vec<_>>();
1148    futures::future::join_all(futs).await;
1149}
1150
1151async fn get_latest_from_peer(
1152    our_genesis_checkpoint_digest: CheckpointDigest,
1153    peer: anemo::Peer,
1154    peer_heights: Arc<RwLock<PeerHeights>>,
1155    timeout: Duration,
1156) {
1157    let peer_id = peer.peer_id();
1158    let mut client = StateSyncClient::new(peer);
1159
1160    let info = {
1161        let maybe_info = peer_heights.read().unwrap().peers.get(&peer_id).copied();
1162
1163        if let Some(info) = maybe_info {
1164            info
1165        } else {
1166            // TODO do we want to create a new API just for querying a node's chainid?
1167            //
1168            // We need to query this node's genesis checkpoint to see if they're on the same chain
1169            // as us
1170            let request = Request::new(GetCheckpointSummaryRequest::BySequenceNumber(0))
1171                .with_timeout(timeout);
1172            let response = client
1173                .get_checkpoint_summary(request)
1174                .await
1175                .map(Response::into_inner);
1176
1177            let info = match response {
1178                Ok(Some(checkpoint)) => {
1179                    let digest = *checkpoint.digest();
1180                    PeerStateSyncInfo {
1181                        genesis_checkpoint_digest: digest,
1182                        on_same_chain_as_us: our_genesis_checkpoint_digest == digest,
1183                        height: *checkpoint.sequence_number(),
1184                        lowest: CheckpointSequenceNumber::default(),
1185                    }
1186                }
1187                Ok(None) => PeerStateSyncInfo {
1188                    genesis_checkpoint_digest: CheckpointDigest::default(),
1189                    on_same_chain_as_us: false,
1190                    height: CheckpointSequenceNumber::default(),
1191                    lowest: CheckpointSequenceNumber::default(),
1192                },
1193                Err(status) => {
1194                    trace!("get_latest_checkpoint_summary request failed: {status:?}");
1195                    return;
1196                }
1197            };
1198            peer_heights
1199                .write()
1200                .unwrap()
1201                .insert_peer_info(peer_id, info);
1202            info
1203        }
1204    };
1205
1206    // Bail early if this node isn't on the same chain as us
1207    if !info.on_same_chain_as_us {
1208        trace!(?info, "Peer {peer_id} not on same chain as us");
1209        return;
1210    }
1211    let Ok(Some((highest_checkpoint, low_watermark))) =
1212        query_peer_for_latest_info(&mut client, timeout).await
1213    else {
1214        return;
1215    };
1216    peer_heights
1217        .write()
1218        .unwrap()
1219        .update_peer_info(peer_id, highest_checkpoint, low_watermark);
1220}
1221
1222/// Queries a peer for their highest_synced_checkpoint and low checkpoint watermark.
1223///
1224/// Distinguishes three outcomes so callers can score the peer:
1225/// - `Ok(Some(..))`: the peer answered with usable latest-checkpoint info.
1226/// - `Ok(None)`: the peer answered but had nothing useful to report (e.g. a pre-upgrade peer
1227///   that returns no latest summary). This is not a failure.
1228/// - `Err(())`: the request itself failed (timeout / transport error). This is a failure: a
1229///   peer that consistently fails these liveness queries is consistently unreachable.
1230async fn query_peer_for_latest_info(
1231    client: &mut StateSyncClient<anemo::Peer>,
1232    timeout: Duration,
1233) -> Result<Option<(Checkpoint, Option<CheckpointSequenceNumber>)>, ()> {
1234    let request = Request::new(()).with_timeout(timeout);
1235    let response = client
1236        .get_checkpoint_availability(request)
1237        .await
1238        .map(Response::into_inner);
1239    match response {
1240        Ok(GetCheckpointAvailabilityResponse {
1241            highest_synced_checkpoint,
1242            lowest_available_checkpoint,
1243        }) => {
1244            return Ok(Some((
1245                highest_synced_checkpoint,
1246                Some(lowest_available_checkpoint),
1247            )));
1248        }
1249        Err(status) => {
1250            // If peer hasn't upgraded they would return 404 NotFound error
1251            if status.status() != anemo::types::response::StatusCode::NotFound {
1252                trace!("get_checkpoint_availability request failed: {status:?}");
1253                return Err(());
1254            }
1255        }
1256    };
1257
1258    // Then we try the old query
1259    // TODO: remove this once the new feature stabilizes
1260    let request = Request::new(GetCheckpointSummaryRequest::Latest).with_timeout(timeout);
1261    let response = client
1262        .get_checkpoint_summary(request)
1263        .await
1264        .map(Response::into_inner);
1265    match response {
1266        Ok(Some(checkpoint)) => Ok(Some((checkpoint, None))),
1267        Ok(None) => Ok(None),
1268        Err(status) => {
1269            trace!("get_checkpoint_summary (latest) request failed: {status:?}");
1270            Err(())
1271        }
1272    }
1273}
1274
1275#[instrument(level = "debug", skip_all)]
1276async fn query_peers_for_their_latest_checkpoint(
1277    network: anemo::Network,
1278    peer_heights: Arc<RwLock<PeerHeights>>,
1279    sender: mpsc::WeakSender<StateSyncMessage>,
1280    timeout: Duration,
1281) {
1282    let peer_heights = &peer_heights;
1283    let futs = peer_heights
1284        .read()
1285        .unwrap()
1286        .peers_on_same_chain()
1287        // Filter out any peers who we aren't connected with
1288        .flat_map(|(peer_id, _info)| network.peer(*peer_id))
1289        .map(|peer| {
1290            let peer_id = peer.peer_id();
1291            let mut client = StateSyncClient::new(peer);
1292
1293            async move {
1294                let start = Instant::now();
1295                let response = query_peer_for_latest_info(&mut client, timeout).await;
1296                let elapsed = start.elapsed();
1297                match response {
1298                    Ok(Some((highest_checkpoint, low_watermark))) => {
1299                        // A peer that answers our periodic liveness/discovery query is healthy;
1300                        // record it so its success rate offsets occasional failures.
1301                        let size = bcs::serialized_size(&highest_checkpoint).unwrap_or(0) as u64;
1302                        let mut peer_heights = peer_heights.write().unwrap();
1303                        peer_heights.record_success(peer_id, size, elapsed);
1304                        peer_heights
1305                            .update_peer_info(peer_id, highest_checkpoint.clone(), low_watermark)
1306                            .then_some(highest_checkpoint)
1307                    }
1308                    // Peer responded but had nothing useful; neutral, do not score.
1309                    Ok(None) => None,
1310                    // Request failed (timeout / transport). A peer that consistently fails
1311                    // these liveness queries is consistently unreachable, so record the
1312                    // failure to feed the scorer even when we are not actively syncing from
1313                    // it. Without this, an unreachable peer is only scored while there is
1314                    // active sync traffic, so it can evade the consistently-failing-peer
1315                    // disconnect logic indefinitely.
1316                    Err(()) => {
1317                        peer_heights.write().unwrap().record_failure(peer_id);
1318                        None
1319                    }
1320                }
1321            }
1322        })
1323        .collect::<Vec<_>>();
1324
1325    debug!("Query {} peers for latest checkpoint", futs.len());
1326
1327    let checkpoints = futures::future::join_all(futs).await.into_iter().flatten();
1328
1329    let highest_checkpoint_seq = checkpoints
1330        .map(|checkpoint| *checkpoint.sequence_number())
1331        .max();
1332
1333    let our_highest_seq = peer_heights
1334        .read()
1335        .unwrap()
1336        .highest_known_checkpoint_sequence_number();
1337
1338    debug!(
1339        "Our highest checkpoint {our_highest_seq:?}, peers' highest checkpoint {highest_checkpoint_seq:?}"
1340    );
1341
1342    let _new_checkpoint = match (highest_checkpoint_seq, our_highest_seq) {
1343        (Some(theirs), None) => theirs,
1344        (Some(theirs), Some(ours)) if theirs > ours => theirs,
1345        _ => return,
1346    };
1347
1348    if let Some(sender) = sender.upgrade() {
1349        let _ = sender.send(StateSyncMessage::StartSyncJob).await;
1350    }
1351}
1352
1353async fn sync_to_checkpoint<S>(
1354    network: anemo::Network,
1355    store: S,
1356    peer_heights: Arc<RwLock<PeerHeights>>,
1357    metrics: Metrics,
1358    pinned_checkpoints: Vec<(CheckpointSequenceNumber, CheckpointDigest)>,
1359    checkpoint_header_download_concurrency: usize,
1360    timeout: Duration,
1361    target_sequence_number: CheckpointSequenceNumber,
1362) -> Result<()>
1363where
1364    S: WriteStore,
1365{
1366    metrics.set_highest_known_checkpoint(target_sequence_number);
1367
1368    let mut current = store
1369        .get_highest_verified_checkpoint()
1370        .expect("store operation should not fail");
1371    if *current.sequence_number() >= target_sequence_number {
1372        return Err(anyhow::anyhow!(
1373            "target checkpoint {} is older than highest verified checkpoint {}",
1374            target_sequence_number,
1375            current.sequence_number(),
1376        ));
1377    }
1378
1379    let peer_balancer = PeerBalancer::new(
1380        &network,
1381        peer_heights.clone(),
1382        PeerCheckpointRequestType::Summary,
1383    );
1384    // range of the next sequence_numbers to fetch
1385    let mut request_stream = (current.sequence_number().checked_add(1).unwrap()
1386        ..=target_sequence_number)
1387        .map(|next| {
1388            let peers = peer_balancer.clone().with_checkpoint(next);
1389            let peer_heights = peer_heights.clone();
1390            let pinned_checkpoints = &pinned_checkpoints;
1391            async move {
1392                if let Some(checkpoint) = peer_heights
1393                    .read()
1394                    .unwrap()
1395                    .get_checkpoint_by_sequence_number(next)
1396                {
1397                    return (Some(checkpoint.to_owned()), next, None);
1398                }
1399
1400                // Iterate through peers trying each one in turn until we're able to
1401                // successfully get the target checkpoint.
1402                for mut peer in peers {
1403                    let peer_id = peer.inner().peer_id();
1404                    let request = Request::new(GetCheckpointSummaryRequest::BySequenceNumber(next))
1405                        .with_timeout(timeout);
1406                    let start = Instant::now();
1407                    let result = peer.get_checkpoint_summary(request).await;
1408                    let elapsed = start.elapsed();
1409
1410                    let checkpoint = match result {
1411                        Ok(response) => match response.into_inner() {
1412                            Some(cp) => Some(cp),
1413                            None => {
1414                                trace!("peer unable to help sync");
1415                                peer_heights.write().unwrap().record_failure(peer_id);
1416                                None
1417                            }
1418                        },
1419                        Err(e) => {
1420                            trace!("{e:?}");
1421                            peer_heights.write().unwrap().record_failure(peer_id);
1422                            None
1423                        }
1424                    };
1425
1426                    let Some(checkpoint) = checkpoint else {
1427                        continue;
1428                    };
1429
1430                    let size = bcs::serialized_size(&checkpoint).expect("serialization should not fail") as u64;
1431                    peer_heights.write().unwrap().record_success(peer_id, size, elapsed);
1432
1433                    // peer didn't give us a checkpoint with the height that we requested
1434                    if *checkpoint.sequence_number() != next {
1435                        tracing::debug!(
1436                            "peer returned checkpoint with wrong sequence number: expected {next}, got {}",
1437                            checkpoint.sequence_number()
1438                        );
1439                        peer_heights
1440                            .write()
1441                            .unwrap()
1442                            .mark_peer_as_not_on_same_chain(peer_id);
1443                        continue;
1444                    }
1445
1446                    // peer gave us a checkpoint whose digest does not match pinned digest
1447                    let checkpoint_digest = checkpoint.digest();
1448                    if let Ok(pinned_digest_index) = pinned_checkpoints.binary_search_by_key(
1449                        checkpoint.sequence_number(),
1450                        |(seq_num, _digest)| *seq_num
1451                    )
1452                        && pinned_checkpoints[pinned_digest_index].1 != *checkpoint_digest
1453                    {
1454                        tracing::debug!(
1455                            "peer returned checkpoint with digest that does not match pinned digest: expected {:?}, got {:?}",
1456                            pinned_checkpoints[pinned_digest_index].1,
1457                            checkpoint_digest
1458                        );
1459                        peer_heights
1460                            .write()
1461                            .unwrap()
1462                            .mark_peer_as_not_on_same_chain(peer.inner().peer_id());
1463                        continue;
1464                    }
1465
1466                    // Insert in our store in the event that things fail and we need to retry
1467                    peer_heights
1468                        .write()
1469                        .unwrap()
1470                        .insert_checkpoint(checkpoint.clone());
1471                    return (Some(checkpoint), next, Some(peer_id));
1472                }
1473                (None, next, None)
1474            }
1475        })
1476        .pipe(futures::stream::iter)
1477        .buffered(checkpoint_header_download_concurrency);
1478
1479    while let Some((maybe_checkpoint, next, maybe_peer_id)) = request_stream.next().await {
1480        assert_eq!(
1481            current
1482                .sequence_number()
1483                .checked_add(1)
1484                .expect("exhausted u64"),
1485            next
1486        );
1487
1488        // Verify the checkpoint
1489        let checkpoint = 'cp: {
1490            let checkpoint = maybe_checkpoint.ok_or_else(|| {
1491                anyhow::anyhow!("no peers were able to help sync checkpoint {next}")
1492            })?;
1493            // Skip verification for manually pinned checkpoints.
1494            if pinned_checkpoints
1495                .binary_search_by_key(checkpoint.sequence_number(), |(seq_num, _digest)| *seq_num)
1496                .is_ok()
1497            {
1498                break 'cp VerifiedCheckpoint::new_unchecked(checkpoint);
1499            }
1500            match verify_checkpoint(&current, &store, checkpoint) {
1501                Ok(verified_checkpoint) => verified_checkpoint,
1502                Err(checkpoint) => {
1503                    let mut peer_heights = peer_heights.write().unwrap();
1504                    // Remove the checkpoint from our temporary store so that we can try querying
1505                    // another peer for a different one
1506                    peer_heights.remove_checkpoint(checkpoint.digest());
1507
1508                    // Mark peer as not on the same chain as us
1509                    if let Some(peer_id) = maybe_peer_id {
1510                        peer_heights.mark_peer_as_not_on_same_chain(peer_id);
1511                    }
1512
1513                    return Err(anyhow::anyhow!(
1514                        "unable to verify checkpoint {checkpoint:?}"
1515                    ));
1516                }
1517            }
1518        };
1519
1520        debug!(checkpoint_seq = ?checkpoint.sequence_number(), "verified checkpoint summary");
1521        if let Some((checkpoint_summary_age_metric, checkpoint_summary_age_metric_deprecated)) =
1522            metrics.checkpoint_summary_age_metrics()
1523        {
1524            checkpoint.report_checkpoint_age(
1525                checkpoint_summary_age_metric,
1526                checkpoint_summary_age_metric_deprecated,
1527            );
1528        }
1529
1530        // Insert the newly verified checkpoint into our store, which will bump our highest
1531        // verified checkpoint watermark as well.
1532        store
1533            .insert_checkpoint(&checkpoint)
1534            .expect("store operation should not fail");
1535
1536        current = checkpoint;
1537    }
1538
1539    peer_heights
1540        .write()
1541        .unwrap()
1542        .cleanup_old_checkpoints(*current.sequence_number());
1543
1544    Ok(())
1545}
1546
1547async fn sync_checkpoint_contents_from_archive<S>(
1548    network: anemo::Network,
1549    archive_config: Option<ArchiveReaderConfig>,
1550    store: S,
1551    peer_heights: Arc<RwLock<PeerHeights>>,
1552    metrics: Metrics,
1553) where
1554    S: WriteStore + Clone + Send + Sync + 'static,
1555{
1556    loop {
1557        sync_checkpoint_contents_from_archive_iteration(
1558            &network,
1559            &archive_config,
1560            store.clone(),
1561            peer_heights.clone(),
1562            metrics.clone(),
1563        )
1564        .await;
1565        tokio::time::sleep(Duration::from_secs(5)).await;
1566    }
1567}
1568
1569async fn sync_checkpoint_contents_from_archive_iteration<S>(
1570    network: &anemo::Network,
1571    archive_config: &Option<ArchiveReaderConfig>,
1572    store: S,
1573    peer_heights: Arc<RwLock<PeerHeights>>,
1574    metrics: Metrics,
1575) where
1576    S: WriteStore + Clone + Send + Sync + 'static,
1577{
1578    let peers: Vec<_> = peer_heights
1579        .read()
1580        .unwrap()
1581        .peers_on_same_chain()
1582        // Filter out any peers who we aren't connected with.
1583        .filter_map(|(peer_id, info)| network.peer(*peer_id).map(|peer| (peer, *info)))
1584        .collect();
1585    let lowest_checkpoint_on_peers = peers
1586        .iter()
1587        .map(|(_p, state_sync_info)| state_sync_info.lowest)
1588        .min();
1589    let highest_synced = store
1590        .get_highest_synced_checkpoint()
1591        .expect("store operation should not fail")
1592        .sequence_number;
1593    let sync_from_archive = if let Some(lowest_checkpoint_on_peers) = lowest_checkpoint_on_peers {
1594        highest_synced < lowest_checkpoint_on_peers
1595    } else {
1596        false
1597    };
1598    debug!(
1599        "Syncing checkpoint contents from archive: {sync_from_archive},  highest_synced: {highest_synced},  lowest_checkpoint_on_peers: {}",
1600        lowest_checkpoint_on_peers.map_or_else(|| "None".to_string(), |l| l.to_string())
1601    );
1602    if sync_from_archive {
1603        let start = highest_synced
1604            .checked_add(1)
1605            .expect("Checkpoint seq num overflow");
1606        let end = lowest_checkpoint_on_peers.unwrap();
1607
1608        let Some(archive_config) = archive_config else {
1609            warn!("Failed to find an archive reader to complete the state sync request");
1610            return;
1611        };
1612        let Some(ingestion_url) = &archive_config.ingestion_url else {
1613            warn!("Archival ingestion url for state sync is not configured");
1614            return;
1615        };
1616        if ingestion_url.contains("checkpoints.mainnet.sui.io") {
1617            warn!("{} can't be used as an archival fallback", ingestion_url);
1618            return;
1619        }
1620        let obj_store = build_object_store(
1621            ingestion_url,
1622            archive_config.remote_store_options.clone(),
1623            archive_config.remote_store_headers.clone(),
1624        );
1625        let mut checkpoint_stream = futures::stream::iter(start..=end)
1626            .map(|seq| {
1627                let obj_store = obj_store.clone();
1628                async move { (seq, fetch_checkpoint(&obj_store, seq).await) }
1629            })
1630            .buffered(archive_config.download_concurrency.get());
1631
1632        while let Some((seq, result)) = checkpoint_stream.next().await {
1633            let checkpoint = match result {
1634                Ok(checkpoint) => checkpoint,
1635                Err(err) => {
1636                    warn!("State sync from archive failed fetching checkpoint {seq}: {err}");
1637                    return;
1638                }
1639            };
1640            if let Err(err) = process_archive_checkpoint(&store, &checkpoint, &metrics) {
1641                warn!("State sync from archive failed processing checkpoint {seq}: {err}");
1642                return;
1643            }
1644        }
1645    }
1646}
1647
1648async fn sync_checkpoint_contents<S>(
1649    network: anemo::Network,
1650    store: S,
1651    peer_heights: Arc<RwLock<PeerHeights>>,
1652    sender: mpsc::WeakSender<StateSyncMessage>,
1653    checkpoint_event_sender: broadcast::Sender<VerifiedCheckpoint>,
1654    checkpoint_content_download_concurrency: usize,
1655    checkpoint_content_download_tx_concurrency: u64,
1656    use_get_checkpoint_contents_v2: bool,
1657    mut target_sequence_channel: watch::Receiver<CheckpointSequenceNumber>,
1658) where
1659    S: WriteStore + Clone,
1660{
1661    let mut highest_synced = store
1662        .get_highest_synced_checkpoint()
1663        .expect("store operation should not fail");
1664
1665    let mut current_sequence = highest_synced.sequence_number().checked_add(1).unwrap();
1666    let mut target_sequence_cursor = 0;
1667    let mut highest_started_network_total_transactions = highest_synced.network_total_transactions;
1668    let mut checkpoint_contents_tasks = FuturesOrdered::new();
1669
1670    let mut tx_concurrency_remaining = checkpoint_content_download_tx_concurrency;
1671
1672    loop {
1673        tokio::select! {
1674            result = target_sequence_channel.changed() => {
1675                match result {
1676                    Ok(()) => {
1677                        target_sequence_cursor = (*target_sequence_channel.borrow_and_update()).checked_add(1).unwrap();
1678                    }
1679                    Err(_) => {
1680                        // Watch channel is closed, exit loop.
1681                        return
1682                    }
1683                }
1684            },
1685            Some(maybe_checkpoint) = checkpoint_contents_tasks.next() => {
1686                match maybe_checkpoint {
1687                    Ok(checkpoint) => {
1688                        let _: &VerifiedCheckpoint = &checkpoint;  // type hint
1689
1690                        store
1691                            .update_highest_synced_checkpoint(&checkpoint)
1692                            .expect("store operation should not fail");
1693                        // We don't care if no one is listening as this is a broadcast channel
1694                        let _ = checkpoint_event_sender.send(checkpoint.clone());
1695                        tx_concurrency_remaining += checkpoint.network_total_transactions - highest_synced.network_total_transactions;
1696                        highest_synced = checkpoint;
1697
1698                    }
1699                    Err(checkpoint) => {
1700                        let _: &VerifiedCheckpoint = &checkpoint;  // type hint
1701                        if let Some(lowest_peer_checkpoint) =
1702                            peer_heights.read().ok().and_then(|x| x.peers.values().map(|state_sync_info| state_sync_info.lowest).min()) {
1703                            if checkpoint.sequence_number() >= &lowest_peer_checkpoint {
1704                                info!("unable to sync contents of checkpoint through state sync {} with lowest peer checkpoint: {}", checkpoint.sequence_number(), lowest_peer_checkpoint);
1705                            }
1706                        } else {
1707                            info!("unable to sync contents of checkpoint through state sync {}", checkpoint.sequence_number());
1708
1709                        }
1710                        // Calculate tx_count for retry by getting previous checkpoint
1711                        let retry_tx_count = if *checkpoint.sequence_number() == 0 {
1712                            checkpoint.network_total_transactions
1713                        } else {
1714                            let prev = store
1715                                .get_checkpoint_by_sequence_number(checkpoint.sequence_number() - 1)
1716                                .expect("previous checkpoint must exist")
1717                                .network_total_transactions;
1718                            checkpoint.network_total_transactions - prev
1719                        };
1720                        // Retry contents sync on failure.
1721                        checkpoint_contents_tasks.push_front(sync_one_checkpoint_contents(
1722                            network.clone(),
1723                            &store,
1724                            peer_heights.clone(),
1725                            use_get_checkpoint_contents_v2,
1726                            retry_tx_count,
1727                            checkpoint,
1728                        ));
1729                    }
1730                }
1731            },
1732        }
1733
1734        // Start new tasks up to configured concurrency limits.
1735        while current_sequence < target_sequence_cursor
1736            && checkpoint_contents_tasks.len() < checkpoint_content_download_concurrency
1737        {
1738            let next_checkpoint = store
1739                .get_checkpoint_by_sequence_number(current_sequence)
1740                .unwrap_or_else(|| panic!(
1741                    "BUG: store should have all checkpoints older than highest_verified_checkpoint (checkpoint {})",
1742                    current_sequence
1743                ));
1744
1745            // Enforce transaction count concurrency limit.
1746            let tx_count = next_checkpoint.network_total_transactions
1747                - highest_started_network_total_transactions;
1748            if tx_count > tx_concurrency_remaining {
1749                break;
1750            }
1751            tx_concurrency_remaining -= tx_count;
1752
1753            highest_started_network_total_transactions = next_checkpoint.network_total_transactions;
1754            current_sequence += 1;
1755            checkpoint_contents_tasks.push_back(sync_one_checkpoint_contents(
1756                network.clone(),
1757                &store,
1758                peer_heights.clone(),
1759                use_get_checkpoint_contents_v2,
1760                tx_count,
1761                next_checkpoint,
1762            ));
1763        }
1764
1765        if highest_synced
1766            .sequence_number()
1767            .is_multiple_of(checkpoint_content_download_concurrency as u64)
1768            || checkpoint_contents_tasks.is_empty()
1769        {
1770            // Periodically notify event loop to notify our peers that we've synced to a new checkpoint height
1771            if let Some(sender) = sender.upgrade() {
1772                let message = StateSyncMessage::SyncedCheckpoint(Box::new(highest_synced.clone()));
1773                let _ = sender.send(message).await;
1774            }
1775        }
1776    }
1777}
1778
1779// Err hands the checkpoint back so the caller can retry it; Ok carries the same type, so boxing
1780// the error would not shrink the Result.
1781#[allow(clippy::result_large_err)]
1782#[instrument(level = "debug", skip_all, fields(sequence_number = ?checkpoint.sequence_number()))]
1783async fn sync_one_checkpoint_contents<S>(
1784    network: anemo::Network,
1785    store: S,
1786    peer_heights: Arc<RwLock<PeerHeights>>,
1787    use_get_checkpoint_contents_v2: bool,
1788    tx_count: u64,
1789    checkpoint: VerifiedCheckpoint,
1790) -> Result<VerifiedCheckpoint, VerifiedCheckpoint>
1791where
1792    S: WriteStore + Clone,
1793{
1794    let (timeout_min, timeout_max) = {
1795        let ph = peer_heights.read().unwrap();
1796        (
1797            ph.checkpoint_content_timeout_min,
1798            ph.checkpoint_content_timeout_max,
1799        )
1800    };
1801    let timeout = compute_adaptive_timeout(tx_count, timeout_min, timeout_max);
1802    debug!(
1803        "syncing checkpoint contents with adaptive timeout {:?} for {} txns",
1804        timeout, tx_count
1805    );
1806
1807    // Check if we already have produced this checkpoint locally. If so, we don't need
1808    // to get it from peers anymore.
1809    if store
1810        .get_highest_synced_checkpoint()
1811        .expect("store operation should not fail")
1812        .sequence_number()
1813        >= checkpoint.sequence_number()
1814    {
1815        debug!("checkpoint was already created via consensus output");
1816        return Ok(checkpoint);
1817    }
1818
1819    // Request checkpoint contents from peers.
1820    let peers = PeerBalancer::new(
1821        &network,
1822        peer_heights.clone(),
1823        PeerCheckpointRequestType::Content,
1824    )
1825    .with_checkpoint(*checkpoint.sequence_number());
1826    let now = tokio::time::Instant::now();
1827    let Some(_contents) = get_full_checkpoint_contents(
1828        peers,
1829        &store,
1830        peer_heights.clone(),
1831        &checkpoint,
1832        use_get_checkpoint_contents_v2,
1833        timeout,
1834    )
1835    .await
1836    else {
1837        // Delay completion in case of error so we don't hammer the network with retries.
1838        let duration = peer_heights
1839            .read()
1840            .unwrap()
1841            .wait_interval_when_no_peer_to_sync_content();
1842        if now.elapsed() < duration {
1843            let duration = duration - now.elapsed();
1844            info!("retrying checkpoint sync after {:?}", duration);
1845            tokio::time::sleep(duration).await;
1846        }
1847        return Err(checkpoint);
1848    };
1849    debug!("completed checkpoint contents sync");
1850    Ok(checkpoint)
1851}
1852
1853#[instrument(level = "debug", skip_all)]
1854async fn get_full_checkpoint_contents<S>(
1855    peers: PeerBalancer,
1856    store: S,
1857    peer_heights: Arc<RwLock<PeerHeights>>,
1858    checkpoint: &VerifiedCheckpoint,
1859    use_get_checkpoint_contents_v2: bool,
1860    timeout: Duration,
1861) -> Option<VersionedFullCheckpointContents>
1862where
1863    S: WriteStore,
1864{
1865    let sequence_number = checkpoint.sequence_number;
1866    let digest = checkpoint.content_digest;
1867    if let Some(contents) = store.get_full_checkpoint_contents(Some(sequence_number), &digest) {
1868        debug!("store already contains checkpoint contents");
1869        return Some(contents);
1870    }
1871
1872    // Iterate through our selected peers trying each one in turn until we're able to
1873    // successfully get the target checkpoint
1874    for mut peer in peers {
1875        let peer_id = peer.inner().peer_id();
1876        debug!(?timeout, "requesting checkpoint contents from {}", peer_id);
1877        let request = Request::new(digest).with_timeout(timeout);
1878        let start = Instant::now();
1879        let result = if use_get_checkpoint_contents_v2 {
1880            peer.get_checkpoint_contents_v2(request).await
1881        } else {
1882            peer.get_checkpoint_contents(request)
1883                .await
1884                .map(|r| r.map(|c| c.map(VersionedFullCheckpointContents::V1)))
1885        };
1886        let elapsed = start.elapsed();
1887
1888        let contents = match result {
1889            Ok(response) => match response.into_inner() {
1890                Some(c) => Some(c),
1891                None => {
1892                    trace!("peer unable to help sync");
1893                    peer_heights.write().unwrap().record_failure(peer_id);
1894                    None
1895                }
1896            },
1897            Err(e) => {
1898                trace!("{e:?}");
1899                peer_heights.write().unwrap().record_failure(peer_id);
1900                None
1901            }
1902        };
1903
1904        let Some(contents) = contents else {
1905            continue;
1906        };
1907
1908        if contents.verify_digests(digest).is_ok() {
1909            let size =
1910                bcs::serialized_size(&contents).expect("serialization should not fail") as u64;
1911            peer_heights
1912                .write()
1913                .unwrap()
1914                .record_success(peer_id, size, elapsed);
1915            let verified_contents = VerifiedCheckpointContents::new_unchecked(contents.clone());
1916            store
1917                .insert_checkpoint_contents(checkpoint, verified_contents)
1918                .expect("store operation should not fail");
1919            return Some(contents);
1920        }
1921    }
1922    debug!("no peers had checkpoint contents");
1923    None
1924}
1925
1926async fn update_checkpoint_watermark_metrics<S>(
1927    mut recv: oneshot::Receiver<()>,
1928    store: S,
1929    metrics: Metrics,
1930) -> Result<()>
1931where
1932    S: WriteStore + Clone + Send + Sync,
1933{
1934    let mut interval = tokio::time::interval(Duration::from_secs(5));
1935    loop {
1936        tokio::select! {
1937             _now = interval.tick() => {
1938                let highest_verified_checkpoint = store.get_highest_verified_checkpoint()
1939                    .expect("store operation should not fail");
1940                metrics.set_highest_verified_checkpoint(highest_verified_checkpoint.sequence_number);
1941                let highest_synced_checkpoint = store.get_highest_synced_checkpoint()
1942                    .expect("store operation should not fail");
1943                metrics.set_highest_synced_checkpoint(highest_synced_checkpoint.sequence_number);
1944             },
1945            _ = &mut recv => break,
1946        }
1947    }
1948    Ok(())
1949}