Skip to main content

snarkos_node_sync/
block_sync.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use crate::{
17    helpers::{PeerPair, PrepareSyncRequest, SyncRequest},
18    locators::BlockLocators,
19};
20use futures::future::BoxFuture;
21use snarkos_node_bft_ledger_service::{BeginLedgerUpdateError, LedgerService};
22use snarkos_node_network::ConnectionMode;
23use snarkos_node_router::messages::DataBlocks;
24use snarkos_node_sync_communication_service::CommunicationService;
25use snarkos_node_sync_locators::{CHECKPOINT_INTERVAL, NUM_RECENT_BLOCKS};
26
27use snarkvm::{
28    console::network::{ConsensusVersion, Network},
29    ledger::{Block, CheckBlockError},
30    utilities::flatten_error,
31};
32
33use anyhow::{Context, Result, anyhow, bail, ensure};
34use futures::FutureExt;
35use indexmap::{IndexMap, IndexSet};
36use itertools::Itertools;
37#[cfg(feature = "locktick")]
38use locktick::parking_lot::{Mutex, RwLock};
39#[cfg(feature = "locktick")]
40use locktick::tokio::Mutex as TMutex;
41#[cfg(not(feature = "locktick"))]
42use parking_lot::Mutex;
43#[cfg(not(feature = "locktick"))]
44use parking_lot::RwLock;
45use rand::seq::{IteratorRandom, SliceRandom};
46use std::{
47    collections::{BTreeMap, HashMap, HashSet, VecDeque, hash_map},
48    net::{IpAddr, Ipv4Addr, SocketAddr},
49    sync::Arc,
50    time::{Duration, Instant},
51};
52#[cfg(not(feature = "locktick"))]
53use tokio::sync::Mutex as TMutex;
54use tokio::sync::Notify;
55use tracing::info;
56
57mod helpers;
58use helpers::rangify_heights;
59
60mod sync_state;
61pub use sync_state::BftSyncMode;
62use sync_state::SyncState;
63
64mod metrics;
65use metrics::BlockSyncMetrics;
66
67// The redundancy factor decreases the possibility of a malicious peers sending us an invalid block locator
68// by requiring multiple peers to advertise the same (prefix of) block locators.
69// However, we do not use this in production yet.
70#[cfg(not(test))]
71pub const REDUNDANCY_FACTOR: usize = 1;
72#[cfg(test)]
73pub const REDUNDANCY_FACTOR: usize = 3;
74
75/// The time nodes wait between issuing batches of block requests to avoid triggering spam detection.
76///
77/// The current rate limit for all messages is around 160k  per second (see [`Gateway::max_cache_events`]).
78/// This constant limits number of block requests to a much lower 100 per second.
79///
80// TODO(kaimast): base rate limits on how many requests were sent to each peer instead.
81pub const BLOCK_REQUEST_BATCH_DELAY: Duration = Duration::from_millis(10);
82
83const EXTRA_REDUNDANCY_FACTOR: usize = REDUNDANCY_FACTOR * 3;
84const NUM_SYNC_CANDIDATE_PEERS: usize = REDUNDANCY_FACTOR * 5;
85
86const BLOCK_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
87
88/// The maximum number of outstanding block requests.
89/// Once a node hits this limit, it will not issue any new requests until existing requests time out or receive responses.
90const MAX_BLOCK_REQUESTS: usize = 50; // 50 requests
91
92/// The maximum number of blocks tolerated before the primary is considered behind its peers.
93pub const MAX_BLOCKS_BEHIND: u32 = 1; // blocks
94
95/// This is a dummy IP address that is used to represent the local node.
96/// Note: This here does not need to be a real IP address, but it must be unique/distinct from all other connections.
97pub const DUMMY_SELF_IP: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0);
98
99/// The map of failed block requests.
100type FailedRequests<H> = BTreeMap<u32, (Option<H>, Option<H>)>;
101
102/// Handle to an outstanding requested, containing the request itself and its timestamp.
103/// This does not contain the response so that checking for responses does not require iterating over all requests.
104#[derive(Clone)]
105struct OutstandingRequest<N: Network> {
106    request: SyncRequest<N>,
107    timestamp: Instant,
108    /// The corresponding response (if any).
109    /// This is guaranteed to be Some if sync_ips for the given request are empty.
110    response: Option<Block<N>>,
111}
112
113/// Information about a block request (used for the REST API).
114#[derive(Clone, serde::Serialize)]
115pub struct BlockRequestInfo {
116    /// Seconds since the request was created
117    elapsed: u64,
118    /// Has the request been responded to?
119    done: bool,
120}
121
122/// Summary of completed all in-flight requests.
123#[derive(Clone, serde::Serialize)]
124pub struct BlockRequestsSummary {
125    outstanding: String,
126    completed: String,
127}
128
129#[derive(thiserror::Error, Debug)]
130pub enum InsertBlockResponseError<N: Network> {
131    #[error("Empty block response")]
132    EmptyBlockResponse,
133    #[error("The peer did not send a consensus version")]
134    NoConsensusVersion,
135    #[error(
136        "The peer's consensus version for height {last_height} is ahead of ours: expected {expected_version}, got {peer_version}"
137    )]
138    ConsensusVersionAhead { peer_version: ConsensusVersion, expected_version: ConsensusVersion, last_height: u32 },
139    #[error(
140        "The peer's consensus version for height {last_height} is behind ours: expected {expected_version}, got {peer_version}"
141    )]
142    ConsensusVersionBehind { peer_version: ConsensusVersion, expected_version: ConsensusVersion, last_height: u32 },
143    #[error("Block Sync already advanced to block {height}")]
144    BlockSyncAlreadyAdvanced { height: u32 },
145    #[error("No such request for height {height}")]
146    NoSuchRequest { height: u32 },
147    #[error("Invalid block hash for height {height} from '{peer_ip}': expected {expected_hash}, got {actual_hash}")]
148    InvalidBlockHash { height: u32, peer_ip: SocketAddr, expected_hash: N::BlockHash, actual_hash: N::BlockHash },
149    #[error(
150        "The previous block hash in candidate block {height} from '{peer_ip}' is incorrect: expected {expected}, but got {actual}"
151    )]
152    InvalidPreviousBlockHash { height: u32, peer_ip: SocketAddr, expected: N::BlockHash, actual: N::BlockHash },
153    #[error("Candidate block {height} from '{peer_ip}' is malformed")]
154    MalformedBlock { height: u32, peer_ip: SocketAddr },
155    #[error("The sync pool did not request block {height} from '{peer_ip}'")]
156    WrongSyncPeer { height: u32, peer_ip: SocketAddr },
157    #[error("{}", flatten_error(.0))]
158    Other(#[from] anyhow::Error),
159}
160
161impl<N: Network> InsertBlockResponseError<N> {
162    /// Returns `true` if the error does not indicate malicious or faulty behavior.
163    pub fn is_benign(&self) -> bool {
164        matches!(self, Self::NoSuchRequest { .. } | Self::BlockSyncAlreadyAdvanced { .. })
165    }
166
167    // Returns true if the peer's consensus version is ahead of ours.
168    pub fn is_consensus_version_ahead(&self) -> bool {
169        matches!(self, Self::ConsensusVersionAhead { .. })
170    }
171
172    // Returns true if the peer's consensus version is behind ours or missing.
173    pub fn is_consensus_version_behind(&self) -> bool {
174        matches!(self, Self::ConsensusVersionBehind { .. } | Self::NoConsensusVersion)
175    }
176}
177
178impl<N: Network> OutstandingRequest<N> {
179    /// Get a reference to the IPs of peers that have not responded to the request (yet).
180    fn sync_ips(&self) -> &IndexSet<SocketAddr> {
181        let (_, _, sync_ips) = &self.request;
182        sync_ips
183    }
184
185    /// Get a mutable reference to the IPs of peers that have not responded to the request (yet).
186    fn sync_ips_mut(&mut self) -> &mut IndexSet<SocketAddr> {
187        let (_, _, sync_ips) = &mut self.request;
188        sync_ips
189    }
190}
191
192/// A struct that keeps track of synchronizing blocks with other nodes.
193///
194/// It generates requests to send to other peers and processes responses to those requests.
195/// The struct also keeps track of block locators, which indicate which peers it can fetch blocks from.
196///
197/// # Notes
198/// - The actual network communication happens in `snarkos_node::Client` (for clients and provers) and in `snarkos_node_bft::Sync` (for validators).
199///
200/// - Validators only sync from other nodes using this struct if they fall behind, e.g.,
201///   because they experience a network partition.
202///   In the common case, validators will generate blocks from the DAG after an anchor certificate has been approved
203///   by a supermajority of the committee.
204pub struct BlockSync<N: Network> {
205    /// The ledger.
206    ledger: Arc<dyn LedgerService<N>>,
207
208    /// The connection mode of this node (Gateway for validators, Router for clients/provers).
209    connection_mode: ConnectionMode,
210
211    /// The map of peer IP to their block locators.
212    /// The block locators are consistent with the ledger and every other peer's block locators.
213    locators: RwLock<HashMap<SocketAddr, BlockLocators<N>>>,
214
215    /// The map of peer-to-peer to their common ancestor.
216    /// This map is used to determine which peers to request blocks from.
217    ///
218    /// Lock ordering: when locking both, `common_ancestors` and `locators`, `common_ancestors` must be locked first.
219    common_ancestors: RwLock<IndexMap<PeerPair, u32>>,
220
221    /// The block requests in progress and their responses.
222    requests: RwLock<BTreeMap<u32, OutstandingRequest<N>>>,
223
224    /// The boolean indicator of whether the node is synced up to the latest block (within the given tolerance).
225    ///
226    /// Lock ordering: if you lock `sync_state` and `requests`, you must lock `sync_state` first.
227    sync_state: RwLock<SyncState>,
228
229    /// The lock used to ensure that [`Self::advance_with_sync_blocks()`] is called by one task at a time.
230    advance_with_sync_blocks_lock: TMutex<()>,
231
232    /// Gets notified when there was an update to the locators or a peer disconnected.
233    peer_notify: Notify,
234
235    /// Gets notified when we received a new block response.
236    response_notify: Notify,
237
238    /// Tracks sync speed
239    metrics: BlockSyncMetrics,
240
241    /// Meta lock that ensures no new block requests are created while a peer is removed.
242    prepare_requests_lock: Mutex<()>,
243
244    /// Tracks failed requests that need to be re-issued.
245    failed_requests: Mutex<FailedRequests<N::BlockHash>>,
246
247    /// Tracks the last time each peer delivered a successful block response.
248    ///
249    /// Used in `handle_block_request_timeouts` to avoid banning peers that are actively
250    /// responding but cannot keep up with the request rate.
251    last_response_at: Mutex<HashMap<SocketAddr, Instant>>,
252
253    /// Condition variable that wakes up waiting tasks when the node is synced.
254    synced_notify: Notify,
255}
256
257impl<N: Network> BlockSync<N> {
258    /// Initializes a new block sync module.
259    pub fn new(ledger: Arc<dyn LedgerService<N>>, connection_mode: ConnectionMode) -> Self {
260        // Make sync state aware of the blocks that already exist on disk at startup.
261        let sync_state = SyncState::new_with_height(ledger.latest_block_height());
262
263        Self {
264            ledger,
265            connection_mode,
266            sync_state: RwLock::new(sync_state),
267            peer_notify: Default::default(),
268            response_notify: Default::default(),
269            locators: Default::default(),
270            requests: Default::default(),
271            common_ancestors: Default::default(),
272            advance_with_sync_blocks_lock: Default::default(),
273            metrics: Default::default(),
274            prepare_requests_lock: Default::default(),
275            failed_requests: Default::default(),
276            last_response_at: Default::default(),
277            synced_notify: Default::default(),
278        }
279    }
280
281    /// Blocks until something about a peer changes,
282    /// or block request has been fully processed (either successfully or unsuccessfully).
283    ///
284    /// Used by the outgoing task.
285    ///
286    /// # Concurrency
287    /// Only one task can wait on this at a time.
288    pub async fn wait_for_peer_update(&self) {
289        self.peer_notify.notified().await
290    }
291
292    /// Blocks until there is a new response to a block request.
293    ///
294    /// Used by the incoming task.
295    ///
296    /// # Concurrency
297    /// Only one task can wait on this at a time.
298    pub async fn wait_for_block_responses(&self) {
299        self.response_notify.notified().await
300    }
301
302    /// Returns `true` if the node is synced up to the latest block (within the given tolerance).
303    #[inline]
304    pub fn is_block_synced(&self) -> bool {
305        self.sync_state.read().is_block_synced()
306    }
307
308    /// This futures blocks until the node is synced.
309    ///
310    /// # Concurrency
311    /// Multiple tasks can wait on this at the same time safely.
312    pub async fn wait_for_synced(&self) {
313        loop {
314            let mut fut = std::pin::pin!(self.synced_notify.notified());
315
316            {
317                let sync_state = self.sync_state.read();
318                if sync_state.is_block_synced() {
319                    return;
320                }
321
322                // Register this task as waiting before dropping the lock.
323                fut.as_mut().enable();
324            }
325
326            fut.await;
327        }
328    }
329
330    /// Similar as [`Self::wait_for_synced`] but returns `None` if the node is already synced.
331    /// Otherwise, it will return a future that behaves like `wait_for_synced`.
332    ///
333    /// # Concurrency
334    /// * This method is atomic, unlike calling `is_synced` and `wait_for_synced` sequentially.
335    /// * Multiple tasks can wait on this at the same time safely.
336    pub fn wait_for_synced_if_syncing(&self) -> Option<BoxFuture<()>> {
337        let mut notified = Box::pin(self.synced_notify.notified());
338
339        {
340            let sync_state = self.sync_state.read();
341            if sync_state.is_block_synced() {
342                return None;
343            }
344
345            // Register this task as waiting before dropping the lock.
346            notified.as_mut().enable();
347        }
348
349        Some(
350            async move {
351                notified.await;
352                self.wait_for_synced().await;
353            }
354            .boxed(),
355        )
356    }
357
358    /// Returns the number of blocks the node is behind the greatest peer height,
359    /// or `None` if no peers are connected yet.
360    #[inline]
361    pub fn num_blocks_behind(&self) -> Option<u32> {
362        self.sync_state.read().num_blocks_behind()
363    }
364
365    /// Returns the greatest block height of any connected peer.
366    #[inline]
367    pub fn greatest_peer_block_height(&self) -> Option<u32> {
368        self.sync_state.read().get_greatest_peer_height()
369    }
370
371    /// Returns the current sync height of this node.
372    /// The sync height is always greater or equal to the ledger height.
373    #[inline]
374    pub fn get_sync_height(&self) -> u32 {
375        self.sync_state.read().get_sync_height()
376    }
377
378    /// Returns the BFT sync mode (fast or DAG), or `None` if no BFT layer is attached.
379    #[inline]
380    pub fn get_bft_sync_mode(&self) -> Option<BftSyncMode> {
381        self.sync_state.read().get_bft_sync_mode()
382    }
383
384    /// Sets the BFT sync mode. Should only be called by the BFT layer.
385    ///
386    /// # Returns
387    /// The previous BFT sync mode (if any).
388    #[inline]
389    pub fn set_bft_sync_mode(&self, mode: BftSyncMode) -> Option<BftSyncMode> {
390        self.sync_state.write().set_bft_sync_mode(mode)
391    }
392
393    /// Returns the number of blocks we requested from peers, but have not received yet.
394    #[inline]
395    pub fn num_outstanding_block_requests(&self) -> usize {
396        self.requests.read().iter().filter(|(_, e)| !e.sync_ips().is_empty()).count()
397    }
398
399    /// The total number of block request, including the ones that have been answered already but not processed yet.
400    #[inline]
401    pub fn num_total_block_requests(&self) -> usize {
402        self.requests.read().len()
403    }
404
405    //// Returns the latest locator height for all known peers.
406    pub fn get_peer_heights(&self) -> HashMap<SocketAddr, u32> {
407        self.locators.read().iter().map(|(addr, locators)| (*addr, locators.latest_locator_height())).collect()
408    }
409
410    //// Returns information about all in-flight block requests.
411    pub fn get_block_requests_info(&self) -> BTreeMap<u32, BlockRequestInfo> {
412        self.requests
413            .read()
414            .iter()
415            .map(|(height, request)| {
416                (*height, BlockRequestInfo {
417                    done: request.sync_ips().is_empty(),
418                    elapsed: request.timestamp.elapsed().as_secs(),
419                })
420            })
421            .collect()
422    }
423
424    /// Returns a summary of all in-flight requests.
425    pub fn get_block_requests_summary(&self) -> BlockRequestsSummary {
426        let requests = self.requests.read();
427        let completed = requests.iter().filter_map(|(h, e)| if e.sync_ips().is_empty() { Some(*h) } else { None });
428        let outstanding = requests.iter().filter_map(|(h, e)| if !e.sync_ips().is_empty() { Some(*h) } else { None });
429
430        BlockRequestsSummary { completed: rangify_heights(completed), outstanding: rangify_heights(outstanding) }
431    }
432
433    pub fn get_sync_speed(&self) -> f64 {
434        self.metrics.get_sync_speed()
435    }
436}
437
438// Helper functions needed for testing
439#[cfg(test)]
440impl<N: Network> BlockSync<N> {
441    /// Returns the latest block height of the given peer IP.
442    fn get_peer_height(&self, peer_ip: &SocketAddr) -> Option<u32> {
443        self.locators.read().get(peer_ip).map(|locators| locators.latest_locator_height())
444    }
445
446    /// Returns the common ancestor for the given peer pair, if it exists.
447    fn get_common_ancestor(&self, peer_a: SocketAddr, peer_b: SocketAddr) -> Option<u32> {
448        self.common_ancestors.read().get(&PeerPair(peer_a, peer_b)).copied()
449    }
450
451    /// Returns the block request for the given height, if it exists.
452    fn get_block_request(&self, height: u32) -> Option<SyncRequest<N>> {
453        self.requests.read().get(&height).map(|e| e.request.clone())
454    }
455
456    /// Returns the timestamp of the last time the block was requested, if it exists.
457    fn get_block_request_timestamp(&self, height: u32) -> Option<Instant> {
458        self.requests.read().get(&height).map(|e| e.timestamp)
459    }
460}
461
462impl<N: Network> BlockSync<N> {
463    /// Returns the block locators.
464    #[inline]
465    pub fn get_block_locators(&self) -> Result<BlockLocators<N>> {
466        // Retrieve the latest block height.
467        let latest_height = self.ledger.latest_block_height();
468
469        // Initialize the recents map.
470        // TODO: generalize this for RECENT_INTERVAL > 1, or remove this comment if we hardwire that to 1
471        let mut recents = IndexMap::with_capacity(NUM_RECENT_BLOCKS);
472        // Retrieve the recent block hashes.
473        for height in latest_height.saturating_sub((NUM_RECENT_BLOCKS - 1) as u32)..=latest_height {
474            recents.insert(height, self.ledger.get_block_hash(height)?);
475        }
476
477        // Initialize the checkpoints map.
478        let mut checkpoints = IndexMap::with_capacity((latest_height / CHECKPOINT_INTERVAL + 1).try_into()?);
479        // Retrieve the checkpoint block hashes.
480        for height in (0..=latest_height).step_by(CHECKPOINT_INTERVAL as usize) {
481            checkpoints.insert(height, self.ledger.get_block_hash(height)?);
482        }
483
484        // Construct the block locators.
485        BlockLocators::new(recents, checkpoints)
486    }
487
488    /// Returns true if there are pending responses to block requests that need to be processed.
489    pub fn has_pending_responses(&self) -> bool {
490        self.requests.read().iter().filter(|(_, req)| req.response.is_some() && req.sync_ips().is_empty()).count() > 0
491    }
492
493    /// Send a batch of block requests.
494    pub async fn send_block_requests<C: CommunicationService>(
495        &self,
496        communication: &C,
497        sync_peers: &IndexMap<SocketAddr, BlockLocators<N>>,
498        requests: &[(u32, PrepareSyncRequest<N>)],
499    ) -> bool {
500        let (start_height, max_num_sync_ips) = match requests.first() {
501            Some((height, (_, _, max_num_sync_ips))) => (*height, *max_num_sync_ips),
502            None => {
503                warn!("Block sync failed - no block requests");
504                return false;
505            }
506        };
507
508        // Use a randomly sampled subset of the sync IPs.
509        let sync_ips: IndexSet<_> =
510            sync_peers.keys().copied().sample(&mut rand::rng(), max_num_sync_ips).into_iter().collect();
511
512        // Calculate the end height.
513        let end_height = start_height.saturating_add(requests.len() as u32);
514
515        // A peer may have disconnected after we prepared this batch; inserting requests that
516        // reference them would leave those requests never cleaned by remove_peer. Hold the same
517        // lock that remove_peer uses so we're atomic: check that all selected peers are still
518        // connected, then insert, without a disconnect slipping in between.
519        {
520            let _prepare_requests_lock = self.prepare_requests_lock.lock();
521            let all_still_connected = {
522                let locators = self.locators.read();
523                sync_ips.iter().all(|ip| locators.contains_key(ip))
524            };
525
526            if !all_still_connected {
527                trace!(
528                    "Skipping block request batch for heights {start_height}-{inclusive_end}: at least one of the selected peer(s) has disconnected",
529                    inclusive_end = end_height.saturating_sub(1)
530                );
531                return false;
532            }
533
534            // Insert the chunk of block requests (still holding lock so remove_peer cannot run).
535            for (height, (hash, previous_hash, _)) in requests.iter() {
536                // Insert the block request into the sync pool using the sync IPs from the last block request in the chunk.
537                if let Err(err) = self.insert_block_request(*height, (*hash, *previous_hash, sync_ips.clone())) {
538                    let err = err.context(format!("Failed to insert block request for height {height}"));
539                    warn!("{}", flatten_error(&err));
540                    return false;
541                }
542            }
543        }
544
545        debug!("Sending {len} block requests to peer(s) at {peers:?}", len = requests.len(), peers = sync_ips);
546
547        /* Send the block request to the peers */
548
549        // Construct the message.
550        let message = C::prepare_block_request(start_height, end_height);
551
552        // Send the message to the peers.
553        let mut tasks = Vec::with_capacity(sync_ips.len());
554        for sync_ip in sync_ips {
555            let sender = communication.send(sync_ip, message.clone()).await;
556            let task = tokio::spawn(async move {
557                // Ensure the request is sent successfully.
558                match sender {
559                    Some(sender) => {
560                        if let Err(err) = sender.await {
561                            warn!("Failed to send block request to peer '{sync_ip}': {err}");
562                            false
563                        } else {
564                            true
565                        }
566                    }
567                    None => {
568                        warn!("Failed to send block request to peer '{sync_ip}': no such peer");
569                        false
570                    }
571                }
572            });
573
574            tasks.push(task);
575        }
576
577        // Wait for all sends to finish at the same time.
578        for result in futures::future::join_all(tasks).await {
579            let success = match result {
580                Ok(success) => success,
581                Err(err) => {
582                    error!("tokio join error: {err}");
583                    false
584                }
585            };
586
587            // If sending fails for any peer, remove the block request from the sync pool.
588            if !success {
589                // Remove the entire block request from the sync pool.
590                let mut requests = self.requests.write();
591                for height in start_height..end_height {
592                    requests.remove(&height);
593                }
594                // Break out of the loop.
595                return false;
596            }
597        }
598        true
599    }
600
601    /// Handles timeouts, checks if block sync is possible, prepares block requests,
602    /// and sends them via the given [`CommunicationService`].
603    ///
604    /// Callers typically call this in a loop after waiting for peer updates, e.g.
605    /// `timeout(MAX_SYNC_INTERVAL, self.wait_for_peer_update())`.
606    pub async fn try_issuing_block_requests<C: CommunicationService>(&self, communication: &C) {
607        self.handle_block_request_timeouts();
608
609        if self.is_block_synced() {
610            trace!("Node is already synced. Will not issue new block requests");
611            return;
612        }
613
614        if !self.sync_state.read().can_issue_new_block_requests() && self.failed_requests.lock().is_empty() {
615            trace!("Nothing to sync. Will not issue new block requests");
616            return;
617        }
618
619        let batches = self.prepare_block_requests();
620
621        if batches.is_empty() {
622            let total_requests = self.num_total_block_requests();
623            let num_outstanding = self.num_outstanding_block_requests();
624            if total_requests != 0 {
625                trace!(
626                    "Not block synced yet, but there are still {total_requests} in-flight requests. {num_outstanding} are still awaiting responses."
627                );
628            } else {
629                debug!(
630                    "Not block synced yet, and there are no outstanding block requests or \
631                 new block requests to send"
632                );
633            }
634        } else {
635            for (block_requests, sync_peers) in batches {
636                for requests in block_requests.chunks(DataBlocks::<N>::MAXIMUM_NUMBER_OF_BLOCKS as usize) {
637                    if !self.send_block_requests(communication, &sync_peers, requests).await {
638                        break;
639                    }
640                    tokio::time::sleep(BLOCK_REQUEST_BATCH_DELAY).await;
641                }
642            }
643        }
644    }
645
646    /// Inserts a new block response from the given peer IP.
647    ///
648    /// Returns an error if the block was malformed, or we already received a different block for this height.
649    /// This function also removes all block requests from the given peer IP on failure.
650    ///
651    /// Note, that this only queues the response. After this, you most likely want to call `Self::try_advancing_block_synchronization`.
652    ///
653    #[inline]
654    pub fn insert_block_responses(
655        &self,
656        peer_ip: SocketAddr,
657        blocks: Vec<Block<N>>,
658        latest_consensus_version: Option<ConsensusVersion>,
659    ) -> Result<(), InsertBlockResponseError<N>> {
660        // Attempt to insert the block responses, and break if we encounter an error.
661        let result = 'outer: {
662            let Some(last_height) = blocks.as_slice().last().map(|b| b.height()) else {
663                break 'outer Err(InsertBlockResponseError::EmptyBlockResponse);
664            };
665
666            let expected_consensus_version =
667                N::CONSENSUS_VERSION(last_height).map_err(InsertBlockResponseError::Other)?;
668
669            // Perform consensus version check, if possible.
670            // This check is only enabled after nodes have reached V12.
671            if expected_consensus_version >= ConsensusVersion::V12 {
672                if let Some(peer_version) = latest_consensus_version {
673                    if peer_version != expected_consensus_version {
674                        break 'outer Err(if peer_version > expected_consensus_version {
675                            InsertBlockResponseError::ConsensusVersionAhead {
676                                peer_version,
677                                expected_version: expected_consensus_version,
678                                last_height,
679                            }
680                        } else {
681                            InsertBlockResponseError::ConsensusVersionBehind {
682                                peer_version,
683                                expected_version: expected_consensus_version,
684                                last_height,
685                            }
686                        });
687                    }
688                } else {
689                    break 'outer Err(InsertBlockResponseError::NoConsensusVersion);
690                }
691            }
692
693            // Insert the candidate blocks into the sync pool.
694            for block in blocks {
695                if let Err(error) = self.insert_block_response(peer_ip, block) {
696                    break 'outer Err(error);
697                }
698            }
699
700            Ok(())
701        };
702
703        // On failure, remove all block requests to the peer.
704        if result.is_err() {
705            self.remove_block_requests_to_peer(&peer_ip);
706        }
707
708        // Return the result.
709        result
710    }
711
712    /// Returns the next block for the given `next_height` if the request is complete,
713    /// or `None` otherwise. This does not remove the block from the `responses` map.
714    #[inline]
715    pub fn peek_next_block(&self, next_height: u32) -> Option<Block<N>> {
716        // Determine if the request is complete:
717        // either there is no request for `next_height`, or the request has no peer socket addresses left.
718        if let Some(entry) = self.requests.read().get(&next_height) {
719            let is_complete = entry.sync_ips().is_empty();
720            if !is_complete {
721                return None;
722            }
723
724            // If the request is complete, return the block from the responses, if there is one.
725            if entry.response.is_none() {
726                warn!("Request for height {next_height} is complete but no response exists");
727            }
728            entry.response.clone()
729        } else {
730            None
731        }
732    }
733
734    /// Attempts to advance synchronization by processing completed block responses.
735    ///
736    /// Returns true, if new blocks were added to the ledger.
737    ///
738    /// # Usage
739    /// This is only called in [`Client::try_block_sync`] and should not be called concurrently by multiple tasks.
740    /// Validators do not call this function, and instead invoke
741    /// [`snarkos_node_bft::Sync::try_advancing_block_synchronization`] which also updates the BFT state.
742    #[inline]
743    pub async fn try_advancing_block_synchronization(&self) -> Result<bool> {
744        // Acquire the lock to ensure this function is called only once at a time.
745        // If the lock is already acquired, return early.
746        //
747        // Note: This lock should not be needed anymore as there is only one place we call it from,
748        // but we keep it for now out of caution.
749        // TODO(kaimast): remove this eventually.
750        let Ok(_lock) = self.advance_with_sync_blocks_lock.try_lock() else {
751            trace!("Skipping attempt to advance block synchronziation as it is already in progress");
752            return Ok(false);
753        };
754
755        // Start with the current height.
756        let mut current_height = self.ledger.latest_block_height();
757        let start_height = current_height;
758        trace!(
759            "Try advancing with block responses (at block {current_height}, current sync speed is {})",
760            self.get_sync_speed()
761        );
762
763        loop {
764            let next_height = current_height + 1;
765
766            let Some(block) = self.peek_next_block(next_height) else {
767                break;
768            };
769
770            // Ensure the block height matches.
771            if block.height() != next_height {
772                warn!("Block height mismatch: expected {}, found {}", current_height + 1, block.height());
773                break;
774            }
775
776            let ledger = self.ledger.clone();
777
778            let (advanced, stop) = tokio::task::spawn_blocking(move || {
779                let ledger_update = match ledger.begin_ledger_update() {
780                    Ok(update) => update,
781                    Err(BeginLedgerUpdateError::ShuttingDown) => {
782                        info!("BlockSync cannot advance the ledger any more. The node is shutting down.");
783                        return Ok((false, true));
784                    }
785                    Err(err) => {
786                        return Err(anyhow!("Unexpected error when beginning ledger update: {err}"));
787                    }
788                };
789
790                // Try to check the next block and advance to it.
791                let block = match ledger_update.check_next_block(block) {
792                    Ok(block) => block,
793                    Err(CheckBlockError::InvalidHeight { .. })
794                    | Err(CheckBlockError::BlockAlreadyExists { .. })
795                    | Err(CheckBlockError::InvalidRound { .. }) => {
796                        debug!("Skipping a block at height {next_height}. The ledger already advanced",);
797                        return Ok((false, false));
798                    }
799                    Err(err) => {
800                        warn!("{err}");
801                        return Err(err.into_anyhow());
802                    }
803                };
804
805                ledger_update.advance_to_next_block(&block).with_context(|| {
806                    format!(
807                        "Failed to advance to next block (height: {height}, hash: {hash})",
808                        height = block.height(),
809                        hash = block.hash(),
810                    )
811                })?;
812
813                Ok((true, false))
814            })
815            .await??;
816
817            // Only count successful advances.
818            // We may not always advance, for example, if the block was already added to the ledger.
819            if advanced {
820                self.count_request_completed();
821            }
822
823            // Remove the block response.
824            self.remove_block_response(next_height);
825
826            // If the node is shutting down, exit the loop.
827            if stop {
828                break;
829            }
830
831            // Update the latest height.
832            current_height = next_height;
833        }
834
835        if current_height > start_height {
836            self.set_sync_height(current_height);
837            Ok(true)
838        } else {
839            Ok(false)
840        }
841    }
842}
843
844impl<N: Network> BlockSync<N> {
845    /// Returns the sync peers with their latest heights, and their minimum common ancestor, if the node can sync.
846    /// This function returns peers that are consistent with each other, and have a block height
847    /// that is greater than the ledger height of this node.
848    ///
849    /// # Locking
850    /// This will read-lock `common_ancestors` and `sync_state`, but not at the same time.
851    pub fn find_sync_peers(&self) -> Option<(IndexMap<SocketAddr, u32>, u32)> {
852        // Retrieve the current sync height.
853        let current_height = self.get_sync_height();
854
855        if let Some((sync_peers, min_common_ancestor)) = self.find_sync_peers_inner(current_height) {
856            // Map the locators into the latest height.
857            let sync_peers =
858                sync_peers.into_iter().map(|(ip, locators)| (ip, locators.latest_locator_height())).collect();
859            // Return the sync peers and their minimum common ancestor.
860            Some((sync_peers, min_common_ancestor))
861        } else {
862            None
863        }
864    }
865
866    /// Updates the block locators and common ancestors for the given peer IP.
867    ///
868    /// This function does not need to check that the block locators are well-formed,
869    /// because that is already done in [`BlockLocators::new()`], as noted in [`BlockLocators`].
870    ///
871    /// This function does **not** check
872    /// that the block locators are consistent with the peer's previous block locators or other peers' block locators.
873    pub fn update_peer_locators(&self, peer_ip: SocketAddr, locators: &BlockLocators<N>) -> Result<()> {
874        let connection_mode = self.connection_mode;
875        // -- First, update the locators entry for the given peer IP. --
876        // We perform this update atomically, and drop the lock as soon as we are done with the update.
877        match self.locators.write().entry(peer_ip) {
878            hash_map::Entry::Occupied(mut e) => {
879                // Return early if the block locators did not change.
880                if e.get() == locators {
881                    return Ok(());
882                }
883
884                let old_height = e.get().latest_locator_height();
885                let new_height = locators.latest_locator_height();
886
887                if old_height > new_height {
888                    debug!("Block height for peer {peer_ip} decreased from {old_height} to {new_height}",);
889                }
890                e.insert(locators.clone());
891            }
892            hash_map::Entry::Vacant(e) => {
893                e.insert(locators.clone());
894            }
895        }
896
897        // -- Second, compute the common ancestor with this node. --
898        let new_local_ancestor = {
899            let mut ancestor = 0;
900            // Attention: Please do not optimize this loop, as it performs fork-detection. In addition,
901            // by iterating upwards, it also early-terminates malicious block locators at the *first* point
902            // of bifurcation in their ledger history, which is a critical safety guarantee provided here.
903            for (height, hash) in locators.clone().into_iter() {
904                if let Ok(ledger_hash) = self.ledger.get_block_hash(height) {
905                    match ledger_hash == hash {
906                        true => ancestor = height,
907                        false => {
908                            warn!(
909                                "[{connection_mode}] Detected fork between this node and peer \"{peer_ip}\" at height {height}"
910                            );
911                            break;
912                        }
913                    }
914                }
915            }
916            ancestor
917        };
918
919        // -- Third, compute the common ancestor with every other peer, and determine if this peer is forked from others. --
920        // Do not hold write lock to `common_ancestors` here, because this can take a while with many peers.
921        let ancestor_updates: Vec<_> = self
922            .locators
923            .read()
924            .iter()
925            .filter_map(|(other_ip, other_locators)| {
926                // Skip if the other peer is the given peer.
927                if other_ip == &peer_ip {
928                    return None;
929                }
930                // Compute the common ancestor with the other peer.
931                let mut ancestor = 0;
932                for (height, hash) in other_locators.clone().into_iter() {
933                    if let Some(expected_hash) = locators.get_hash(height) {
934                        match expected_hash == hash {
935                            true => ancestor = height,
936                            false => {
937                                debug!(
938                                    "[{connection_mode}] Detected fork between peers \"{other_ip}\" and \"{peer_ip}\" at height {height}"
939                                );
940                                break;
941                            }
942                        }
943                    }
944                }
945
946                Some((PeerPair(peer_ip, *other_ip), ancestor))
947            })
948            .collect();
949
950        // -- Forth, update the map of common ancestors. --
951        // Scope the lock, so it is dropped before locking `sync_state`.
952        {
953            let mut common_ancestors = self.common_ancestors.write();
954            common_ancestors.insert(PeerPair(DUMMY_SELF_IP, peer_ip), new_local_ancestor);
955
956            for (peer_pair, new_ancestor) in ancestor_updates.into_iter() {
957                common_ancestors.insert(peer_pair, new_ancestor);
958            }
959        }
960
961        // -- Finally, update sync state and notify the sync loop about the change. --
962        let is_synced = if let Some(greatest_peer_height) =
963            self.locators.read().values().map(|l| l.latest_locator_height()).max()
964        {
965            let mut sync_state = self.sync_state.write();
966            sync_state.set_greatest_peer_height(greatest_peer_height);
967            sync_state.is_block_synced()
968        } else {
969            error!("Got new block locators but greatest peer height is zero.");
970            false
971        };
972
973        // For the unlikely case a peer's height gets lowered.
974        if is_synced {
975            self.synced_notify.notify_waiters();
976        }
977
978        // Even if the greatest peer height did not change, we still received new block locators
979        // that the sync loop might need to proceed.
980        self.peer_notify.notify_one();
981
982        Ok(())
983    }
984
985    /// TODO (howardwu): Remove the `common_ancestor` entry. But check that this is safe
986    ///  (that we don't rely upon it for safety when we re-connect with the same peer).
987    /// Removes the peer from the sync pool, if they exist.
988    pub fn remove_peer(&self, peer_ip: &SocketAddr) {
989        trace!("Removing peer {peer_ip} from block sync");
990
991        // Ensure no new block requests are issued to this peer, while it is disconnecting.
992        let _prepare_requests_lock = self.prepare_requests_lock.lock();
993
994        // Remove the locators entry for the given peer IP.
995        self.locators.write().remove(peer_ip);
996        // Remove all common ancestor entries for this peers.
997        self.common_ancestors.write().retain(|pair, _| !pair.contains(peer_ip));
998        // Drop the last-response timestamp so a reconnecting peer starts fresh.
999        self.last_response_at.lock().remove(peer_ip);
1000        // Remove all block requests to the peer.
1001        self.remove_block_requests_to_peer(peer_ip);
1002
1003        let synced = {
1004            // Do not lock sync state and locators at the same time.
1005            let max_height = self.locators.read().values().map(|l| l.latest_locator_height()).max();
1006            let mut sync_state = self.sync_state.write();
1007
1008            // Update sync state, because the greatest peer height may have decreased.
1009            if let Some(greatest_peer_height) = max_height {
1010                sync_state.set_greatest_peer_height(greatest_peer_height);
1011            } else {
1012                // There are no more peers left.
1013                sync_state.clear_greatest_peer_height();
1014            }
1015
1016            sync_state.is_block_synced()
1017        };
1018
1019        // For the case where the maximum peer height gets lowered.
1020        if synced {
1021            self.synced_notify.notify_waiters();
1022        }
1023
1024        // Notify the sync loop that something changed.
1025        self.peer_notify.notify_one();
1026    }
1027}
1028
1029// Helper type for prepare_block_requests
1030pub type BlockRequestBatch<N> = (Vec<(u32, PrepareSyncRequest<N>)>, IndexMap<SocketAddr, BlockLocators<N>>);
1031
1032impl<N: Network> BlockSync<N> {
1033    /// Returns a list of block requests and the sync peers, if the node needs to sync.
1034    ///
1035    /// You usually want to call `remove_timed_out_block_requests` before invoking this function.
1036    ///
1037    /// # Returns
1038    /// * An empty vector, if there is no work to be done.
1039    /// * Otherwise, a vector of block request batches, each with a contiguous range of heights.
1040    ///
1041    /// # Concurrency
1042    /// This should be called by at most one task at a time.
1043    ///
1044    /// # Usage
1045    ///  - For validators, the primary spawns exactly one task that periodically calls
1046    ///    `bft::Sync::try_issuing_block_requests`. There is no possibility of concurrent calls to it.
1047    ///  - For clients, `Client::initialize_sync` spawn exactly one task that periodically calls
1048    ///    `Client::try_issuing_block_requests` which calls this function.
1049    ///  - Provers do not call this function.
1050    pub fn prepare_block_requests(&self) -> Vec<BlockRequestBatch<N>> {
1051        let _block_requests_lock = self.prepare_requests_lock.lock();
1052
1053        // Used to print more information when we max out on requests.
1054        let print_requests = || {
1055            if tracing::enabled!(tracing::Level::TRACE) {
1056                let summary = self.get_block_requests_summary();
1057
1058                if summary.completed.is_empty() {
1059                    trace!("There are no completed requests that have not been processed yet.");
1060                } else {
1061                    trace!("The following requests are complete but not processed yet: {:?}", summary.completed);
1062                }
1063
1064                if summary.outstanding.is_empty() {
1065                    trace!("There are no outstanding requests.");
1066                } else {
1067                    trace!("The following requests are still outstanding: {:?}", summary.outstanding);
1068                }
1069            }
1070        };
1071
1072        // Do not hold lock here as, currently, `find_sync_peers_inner` can take a while.
1073        let current_height = self.get_sync_height();
1074
1075        // Determine if there are any failed requests that need to be re-issued.
1076        //
1077        // The entries are only removed once the requests are successfully re-issued.
1078        let mut failed_requests = self.failed_requests.lock();
1079
1080        // Ensure none of the failed requests are obsolete.
1081        while let Some(height) = failed_requests.keys().next()
1082            && *height <= current_height
1083        {
1084            failed_requests.pop_first();
1085        }
1086
1087        // Re-issue the remaining failed requests.
1088        if !failed_requests.is_empty() {
1089            trace!("There are {} failed requests that need to be re-issued.", failed_requests.len());
1090
1091            // Convert the set of failed requests into one or multiple continuous ranges.
1092            let iter = failed_requests.iter();
1093            let mut batches: VecDeque<Vec<(u32, _, _)>> = VecDeque::new();
1094
1095            for (height, (hash, previous_hash)) in iter {
1096                if let Some(prev_batch) = batches.back_mut() {
1097                    if let Some((last_height, _, _)) = prev_batch.last()
1098                        && *last_height + 1 != *height
1099                    {
1100                        // We need to start a new batch.
1101                        batches.push_back(vec![(*height, *hash, *previous_hash)]);
1102                    } else {
1103                        // We can add the request to the current batch.
1104                        prev_batch.push((*height, *hash, *previous_hash));
1105                    }
1106                } else {
1107                    // First batch.
1108                    batches.push_back(vec![(*height, *hash, *previous_hash)]);
1109                }
1110            }
1111
1112            let mut result = vec![];
1113            while let Some(batch) = batches.pop_front() {
1114                // SAFETY: Batches are guaranteed to be non-empty.
1115                let start_height = batch.first().unwrap().0;
1116                let end_height = batch.last().unwrap().0 + 1;
1117
1118                // Set the maximum number of blocks, so that they do not exceed the end height.
1119                let max_new_blocks_to_request = end_height - start_height;
1120
1121                let Some((sync_peers, min_common_ancestor)) = self.find_sync_peers_inner(start_height) else {
1122                    // This generally shouldn't happen, because there cannot be outstanding requests when no peers are connected.
1123                    warn!("Cannot re-request blocks because no or not enough peers are connected");
1124                    return result;
1125                };
1126
1127                // Retrieve the greatest block height of any connected peer.
1128                let Some(greatest_peer_height) = sync_peers.values().map(|l| l.latest_locator_height()).max() else {
1129                    // This should never happen because `sync_peers` is guaranteed to be non-empty.
1130                    error!(
1131                        "Cannot re-request blocks because no or not enough peers with sufficient height are connected"
1132                    );
1133                    return result;
1134                };
1135
1136                // (Try to) construct the requests.
1137                let requests = self.construct_requests(
1138                    &sync_peers,
1139                    start_height.saturating_sub(1),
1140                    min_common_ancestor,
1141                    max_new_blocks_to_request,
1142                    greatest_peer_height,
1143                );
1144
1145                // Only remove from failed_requests the heights we actually re-issued.
1146                // (If construct_requests returned empty we must not drop these failed requests.)
1147                for (height, _) in &requests {
1148                    failed_requests.remove(height);
1149                }
1150
1151                result.push((requests, sync_peers));
1152            }
1153
1154            return result;
1155        }
1156
1157        // Ensure to not exceed the maximum number of outstanding block requests.
1158        let max_outstanding_block_requests =
1159            (MAX_BLOCK_REQUESTS as u32) * (DataBlocks::<N>::MAXIMUM_NUMBER_OF_BLOCKS as u32);
1160
1161        // Ensure there is a finite bound on the number of block respnoses we receive, that have not been processed yet.
1162        let max_total_requests = 4 * max_outstanding_block_requests;
1163
1164        let max_new_blocks_to_request =
1165            max_outstanding_block_requests.saturating_sub(self.num_outstanding_block_requests() as u32);
1166
1167        // Prepare the block requests and sync peers, or returns an empty result if there is nothing to request.
1168        if self.num_total_block_requests() >= max_total_requests as usize {
1169            trace!(
1170                "We are already requested at least {max_total_requests} blocks that have not been fully processed yet. Will not issue more."
1171            );
1172
1173            print_requests();
1174            vec![]
1175        } else if max_new_blocks_to_request == 0 {
1176            trace!(
1177                "Already reached the maximum number of outstanding blocks ({max_outstanding_block_requests}). Will not issue more."
1178            );
1179
1180            print_requests();
1181            vec![]
1182        } else if let Some((sync_peers, min_common_ancestor)) = self.find_sync_peers_inner(current_height) {
1183            // Retrieve the greatest block height of any connected peer.
1184            // We do not need to update the sync state here, as that already happens when the block locators are received.
1185            let greatest_peer_height = sync_peers.values().map(|l| l.latest_locator_height()).max().unwrap_or(0);
1186
1187            // Construct the list of block requests.
1188            let requests = self.construct_requests(
1189                &sync_peers,
1190                current_height,
1191                min_common_ancestor,
1192                max_new_blocks_to_request,
1193                greatest_peer_height,
1194            );
1195
1196            if !requests.is_empty() {
1197                trace!(
1198                    "Generated new block requests for the following heights: {}",
1199                    rangify_heights(requests.iter().map(|(h, _)| *h))
1200                );
1201            }
1202
1203            vec![(requests, sync_peers)]
1204        } else if self.requests.read().is_empty() {
1205            // This can happen during a race condition where the node just finished syncing.
1206            // It does not make sense to log or change the sync status here.
1207            // Checking the sync status here also does not make sense, as the node might as well have switched back
1208            //  from `synced` to `syncing` between calling `find_sync_peers_inner` and this line.
1209
1210            vec![]
1211        } else {
1212            // This happens if we already requested all advertised blocks.
1213            trace!("No new blocks can be requested, but there are still outstanding requests.");
1214
1215            print_requests();
1216            vec![]
1217        }
1218    }
1219
1220    /// Should only be called by validators when they successfully process a block request.
1221    /// (for other nodes this will be automatically called internally)
1222    ///
1223    /// TODO(kaimast): remove this public function once the sync logic is fully unified `BlockSync`.
1224    pub fn count_request_completed(&self) {
1225        self.metrics.count_request_completed();
1226    }
1227
1228    /// Set the sync height to a the given value.
1229    /// This is a no-op if `new_height` is equal or less to the current sync height.
1230    pub fn set_sync_height(&self, new_height: u32) {
1231        // Scope state lock to avoid locking state and metrics at the same time.
1232        let (synced, fully_synced) = {
1233            let mut state = self.sync_state.write();
1234            state.set_sync_height(new_height);
1235            (state.is_block_synced(), !state.can_issue_new_block_requests())
1236        };
1237
1238        if fully_synced {
1239            self.metrics.mark_fully_synced();
1240        }
1241
1242        if synced {
1243            self.synced_notify.notify_waiters();
1244        }
1245    }
1246
1247    /// Inserts a block request for the given height.
1248    ///
1249    /// With a single task issuing block requests, a height should not already be in the requests
1250    /// map: heights in `failed_requests` were removed from `requests` when added there, and
1251    /// `construct_requests` skips heights already in `requests`. If this returns "already in
1252    /// requests map", logging the existing entry (e.g. has response? peers still in locators?)
1253    /// may help diagnose why the height was included in the batch.
1254    fn insert_block_request(&self, height: u32, (hash, previous_hash, sync_ips): SyncRequest<N>) -> Result<()> {
1255        // Ensure the block request does not already exist.
1256        self.check_block_request(height)?;
1257        // Ensure the sync IPs are not empty.
1258        ensure!(!sync_ips.is_empty(), "Cannot insert a block request with no sync IPs");
1259        // Insert the block request.
1260        self.requests.write().insert(height, OutstandingRequest {
1261            request: (hash, previous_hash, sync_ips),
1262            timestamp: Instant::now(),
1263            response: None,
1264        });
1265        Ok(())
1266    }
1267
1268    /// Inserts the given block response, after checking that the request exists and the response is well-formed.
1269    /// On success, this function removes the peer IP from the request sync peers and inserts the response.
1270    fn insert_block_response(&self, peer_ip: SocketAddr, block: Block<N>) -> Result<(), InsertBlockResponseError<N>> {
1271        // Retrieve the block height.
1272        let height = block.height();
1273        let mut requests = self.requests.write();
1274
1275        if self.ledger.contains_block_height(height) {
1276            return Err(InsertBlockResponseError::BlockSyncAlreadyAdvanced { height });
1277        }
1278
1279        let Some(entry) = requests.get_mut(&height) else {
1280            return Err(InsertBlockResponseError::NoSuchRequest { height });
1281        };
1282
1283        // Retrieve the request entry for the candidate block.
1284        let (expected_hash, expected_previous_hash, sync_ips) = &entry.request;
1285
1286        // Ensure the candidate block hash matches the expected hash.
1287        if let Some(expected_hash) = expected_hash
1288            && block.hash() != *expected_hash
1289        {
1290            return Err(InsertBlockResponseError::InvalidBlockHash {
1291                height,
1292                peer_ip,
1293                expected_hash: *expected_hash,
1294                actual_hash: block.hash(),
1295            });
1296        }
1297        // Ensure the previous block hash matches if it exists.
1298        if let Some(expected_previous_hash) = expected_previous_hash
1299            && block.previous_hash() != *expected_previous_hash
1300        {
1301            return Err(InsertBlockResponseError::InvalidPreviousBlockHash {
1302                height,
1303                peer_ip,
1304                expected: *expected_previous_hash,
1305                actual: block.previous_hash(),
1306            });
1307        }
1308        // Ensure the sync pool requested this block from the given peer.
1309        if !sync_ips.contains(&peer_ip) {
1310            return Err(InsertBlockResponseError::WrongSyncPeer { height, peer_ip });
1311        }
1312
1313        // Remove the peer IP from the request entry.
1314        entry.sync_ips_mut().swap_remove(&peer_ip);
1315
1316        if let Some(existing_block) = &entry.response {
1317            // If the candidate block was already present, ensure it is the same block.
1318            if block != *existing_block {
1319                return Err(InsertBlockResponseError::MalformedBlock { height, peer_ip });
1320            }
1321        } else {
1322            entry.response = Some(block.clone());
1323        }
1324
1325        trace!("Received a new and valid block response for height {height}");
1326
1327        // Record that this peer is actively responding. Used by `handle_block_request_timeouts`
1328        // to avoid banning peers that are slow but making progress.
1329        self.last_response_at.lock().insert(peer_ip, Instant::now());
1330
1331        // Notify the sync loop that something changed.
1332        self.response_notify.notify_one();
1333
1334        Ok(())
1335    }
1336
1337    /// Checks that a block request for the given height does not already exist.
1338    fn check_block_request(&self, height: u32) -> Result<()> {
1339        // Ensure the block height is not already in the ledger.
1340        if self.ledger.contains_block_height(height) {
1341            bail!("Failed to add block request, as block {height} exists in the ledger");
1342        }
1343        // Ensure the block height is not already requested.
1344        if self.requests.read().contains_key(&height) {
1345            bail!("Failed to add block request, as block {height} exists in the requests map");
1346        }
1347
1348        Ok(())
1349    }
1350
1351    /// Removes the block request and response for the given height
1352    /// This may only be called after `peek_next_block`, which checked if the request for the given height was complete.
1353    ///
1354    /// Precondition: This may only be called after `peek_next_block` has returned `Some`,
1355    /// which has checked if the request for the given height is complete
1356    /// and there is a block with the given `height` in the `responses` map.
1357    pub fn remove_block_response(&self, height: u32) {
1358        // Remove the request entry for the given height.
1359        if let Some(e) = self.requests.write().remove(&height) {
1360            trace!(
1361                "Block request for height {height} was completed in {}ms (sync speed is {})",
1362                e.timestamp.elapsed().as_millis(),
1363                self.get_sync_speed()
1364            );
1365
1366            // Notify the sending task that less requests are in-flight.
1367            self.peer_notify.notify_one();
1368        }
1369    }
1370
1371    /// Removes all block requests for the given peer IP.
1372    ///
1373    /// This is used when disconnecting from a peer or when a peer sends invalid block responses.
1374    fn remove_block_requests_to_peer(&self, peer_ip: &SocketAddr) {
1375        trace!("Block sync is removing all block requests to peer {peer_ip}...");
1376        let mut heights = vec![];
1377        let mut removed_requests = vec![];
1378
1379        // Remove the peer IP from the requests map. If any request entry is now empty,
1380        // and its corresponding response entry is also empty, then remove that request entry altogether.
1381        self.requests.write().retain(|height, e| {
1382            let had_peer = e.sync_ips_mut().swap_remove(peer_ip);
1383
1384            if had_peer && e.response.is_none() {
1385                trace!("Removed outstanding block request to peer {peer_ip} at height {height}");
1386                heights.push(*height);
1387            }
1388
1389            // Only remove requests that were sent to this peer, that have no other peer that can respond instead,
1390            // and that were not completed yet.
1391            let retain = !had_peer || !e.sync_ips().is_empty() || e.response.is_some();
1392            if !retain {
1393                // Record the request to be re-issued.
1394                let (hash, previous_hash, _) = &e.request;
1395                removed_requests.push((*height, (*hash, *previous_hash)));
1396            }
1397            retain
1398        });
1399
1400        if !heights.is_empty() {
1401            debug!(
1402                "Removed outstanding block requests to disconnecting peer '{peer_ip}' at heights: {}. {} were fully removed.",
1403                rangify_heights(heights),
1404                removed_requests.len(),
1405            );
1406        }
1407
1408        // Mark all requests that were removed as failed.
1409        if !removed_requests.is_empty() {
1410            let mut failed_requests = self.failed_requests.lock();
1411            for (height, e) in removed_requests.into_iter() {
1412                let prev = failed_requests.insert(height, e);
1413                if prev.is_some() {
1414                    warn!(
1415                        "Failed to mark block request at height {height} as failed, as it already exists in the failed requests map"
1416                    );
1417                }
1418            }
1419        }
1420
1421        // No need to remove responses here, because requests with responses will be retained.
1422    }
1423
1424    /// Removes block requests that have timed out, i.e, requests we sent that did not receive a response in time.
1425    ///
1426    /// Timed-out requests will be marked as "failed" and re-issued on the next call to `prepare_block_requests`.
1427    pub fn handle_block_request_timeouts(&self) {
1428        // Snapshot last-response times before locking `requests`. A request whose assigned peer
1429        // has responded within `BLOCK_REQUEST_TIMEOUT` is not timed out, even if its own timer
1430        // has elapsed — the peer is keeping up with a backlog and timing this request out would
1431        // just churn it through `failed_requests` and lose its place in the queue.
1432        let responsive_peers: HashSet<SocketAddr> = {
1433            let last_response_at = self.last_response_at.lock();
1434            let now = Instant::now();
1435            last_response_at
1436                .iter()
1437                .filter_map(|(peer, t)| (now.duration_since(*t) <= BLOCK_REQUEST_TIMEOUT).then_some(*peer))
1438                .collect()
1439        };
1440
1441        // Avoid locking `locators` and `requests` at the same time.
1442        let (timed_out_requests, peers_to_ban) = {
1443            // Acquire the write lock on the requests map.
1444            let mut requests = self.requests.write();
1445
1446            // Retrieve the current time.
1447            let now = Instant::now();
1448
1449            // Retrieve the current block height
1450            let current_height = self.ledger.latest_block_height();
1451
1452            // Track the number of timed out block requests (only used to print a log message).
1453            let mut timed_out_requests = vec![];
1454
1455            // Track which peers should be banned due to unresponsiveness.
1456            let mut peers_to_ban: HashSet<SocketAddr> = HashSet::new();
1457
1458            // Remove timed out block requests.
1459            requests.retain(|height, e| {
1460                let is_obsolete = *height <= current_height;
1461                // Determine if the duration since the request timestamp has exceeded the request timeout.
1462                let timer_elapsed = now.duration_since(e.timestamp) > BLOCK_REQUEST_TIMEOUT;
1463                // Determine if the request is complete.
1464                let is_complete = e.sync_ips().is_empty() && e.response.is_some();
1465                // If any assigned peer is still actively responding, the request is not stuck.
1466                let has_responsive_peer = e.sync_ips().iter().any(|ip| responsive_peers.contains(ip));
1467
1468                // Determine if the request has timed out.
1469                let is_timeout = timer_elapsed && !is_complete && !has_responsive_peer;
1470
1471                // Retain if this is not a timeout and is not obsolete.
1472                let retain = !is_timeout && !is_obsolete;
1473
1474                if is_timeout {
1475                    trace!("Block request at height {height} has timed out: timer_elapsed={timer_elapsed}, is_complete={is_complete}, is_obsolete={is_obsolete}");
1476
1477                    // Increment the number of timed out block requests.
1478                    let (hash, previous_hash, _) = &e.request;
1479                    timed_out_requests.push((*height, (*hash, *previous_hash)));
1480                } else if is_obsolete {
1481                    trace!("Block request at height {height} became obsolete (current_height={current_height})");
1482                }
1483
1484                // If the request timed out, also remove and ban given peer.
1485                if is_timeout {
1486                    for peer_ip in e.sync_ips().iter() {
1487                        peers_to_ban.insert(*peer_ip);
1488                    }
1489                }
1490
1491                retain
1492            });
1493
1494            if !timed_out_requests.is_empty() {
1495                debug!(
1496                    "{num} block requests timed out: {list}",
1497                    num = timed_out_requests.len(),
1498                    list = rangify_heights(timed_out_requests.iter().map(|(height, _)| *height))
1499                );
1500            }
1501
1502            (timed_out_requests, peers_to_ban)
1503        };
1504
1505        // Mark the non-obsolete requests that timed out as failed.
1506        if !timed_out_requests.is_empty() {
1507            let mut failed_requests = self.failed_requests.lock();
1508            for (height, e) in timed_out_requests.into_iter() {
1509                let prev = failed_requests.insert(height, e);
1510                if prev.is_some() {
1511                    warn!(
1512                        "Failed to mark block request at height {height} as failed, as it already exists in the failed requests map"
1513                    );
1514                }
1515            }
1516        }
1517
1518        // Remove and ban the unresponsive peers. The `has_responsive_peer` check inside `retain`
1519        // above already guarantees that a request only counts as timed out when none of its
1520        // assigned peers have responded recently, so every peer in this set is unresponsive.
1521        for peer_ip in peers_to_ban {
1522            self.remove_peer(&peer_ip);
1523            // TODO: Uncomment this when we have a more rigorous analysis and testing of peer banning.
1524            // peer_pool_handler.ip_ban_peer(peer_ip, Some("timed out on block requests"));
1525        }
1526    }
1527
1528    /// Finds the peers to sync from and the shared common ancestor, starting at the give height.
1529    ///
1530    /// Unlike [`Self::find_sync_peers`] this does not only return the latest locators height, but the full BlockLocators for each peer.
1531    /// Returns `None` if there are no peers to sync from.
1532    ///
1533    /// # Locking
1534    /// This function will read-lock `common_ancestors`.
1535    fn find_sync_peers_inner(&self, current_height: u32) -> Option<(IndexMap<SocketAddr, BlockLocators<N>>, u32)> {
1536        // Retrieve the latest ledger height.
1537        let latest_ledger_height = self.ledger.latest_block_height();
1538
1539        // Pick a set of peers above the latest ledger height, and include their locators.
1540        // This will sort the peers by locator height in descending order.
1541        let candidate_locators: IndexMap<_, _> = self
1542            .locators
1543            .read()
1544            .iter()
1545            .filter(|(_, locators)| locators.latest_locator_height() > current_height)
1546            .sorted_by(|(_, a), (_, b)| b.latest_locator_height().cmp(&a.latest_locator_height()))
1547            .take(NUM_SYNC_CANDIDATE_PEERS)
1548            .map(|(peer_ip, locators)| (*peer_ip, locators.clone()))
1549            .collect();
1550
1551        // Case 0: If there are no candidate peers, return `None`.
1552        if candidate_locators.is_empty() {
1553            trace!("Found no sync peers with height greater {current_height}");
1554            return None;
1555        }
1556
1557        // TODO (howardwu): Change this to the highest cumulative weight for Phase 3.
1558        // Case 1: If all of the candidate peers share a common ancestor below the latest ledger height,
1559        // then pick the peer with the highest height, and find peers (up to extra redundancy) with
1560        // a common ancestor above the block request range. Set the end height to their common ancestor.
1561
1562        // Determine the threshold number of peers to sync from.
1563        let threshold_to_request = candidate_locators.len().min(REDUNDANCY_FACTOR);
1564
1565        // Breaks the loop when the first threshold number of peers are found, biasing for the peer with the highest height
1566        // and a cohort of peers who share a common ancestor above this node's latest ledger height.
1567        for (idx, (peer_ip, peer_locators)) in candidate_locators.iter().enumerate() {
1568            // The height of the common ancestor shared by all selected peers.
1569            let mut min_common_ancestor = peer_locators.latest_locator_height();
1570
1571            // The peers we will synchronize from.
1572            // As the previous iteration did not succeed, restart with the next candidate peers.
1573            let mut sync_peers = vec![(*peer_ip, peer_locators.clone())];
1574
1575            // Try adding other peers consistent with this one to the sync peer set.
1576            for (other_ip, other_locators) in candidate_locators.iter().skip(idx + 1) {
1577                // Check if these two peers have a common ancestor above the latest ledger height.
1578                if let Some(common_ancestor) = self.common_ancestors.read().get(&PeerPair(*peer_ip, *other_ip)) {
1579                    // If so, then check that their block locators are consistent.
1580                    if *common_ancestor > latest_ledger_height && peer_locators.is_consistent_with(other_locators) {
1581                        // If their common ancestor is less than the minimum common ancestor, then update it.
1582                        min_common_ancestor = min_common_ancestor.min(*common_ancestor);
1583
1584                        // Add the other peer to the list of sync peers.
1585                        sync_peers.push((*other_ip, other_locators.clone()));
1586                    }
1587                }
1588            }
1589
1590            // If we have enough sync peers above the latest ledger height, finish and return them.
1591            if min_common_ancestor > latest_ledger_height && sync_peers.len() >= threshold_to_request {
1592                // Shuffle the sync peers prior to returning. This ensures the rest of the stack
1593                // does not rely on the order of the sync peers, and that the sync peers are not biased.
1594                sync_peers.shuffle(&mut rand::rng());
1595
1596                // Collect into an IndexMap and return.
1597                return Some((sync_peers.into_iter().collect(), min_common_ancestor));
1598            }
1599        }
1600
1601        // If there is not enough peers with a minimum common ancestor above the latest ledger height, return None.
1602        None
1603    }
1604
1605    /// Given the sync peers and their minimum common ancestor, return a list of block requests.
1606    ///
1607    /// # Returns
1608    /// The list of block requests, ordered by height.
1609    fn construct_requests(
1610        &self,
1611        sync_peers: &IndexMap<SocketAddr, BlockLocators<N>>,
1612        sync_height: u32,
1613        min_common_ancestor: u32,
1614        max_blocks_to_request: u32,
1615        greatest_peer_height: u32,
1616    ) -> Vec<(u32, PrepareSyncRequest<N>)> {
1617        // Compute the start height for the block requests.
1618        let start_height = {
1619            let requests = self.requests.read();
1620            let ledger_height = self.ledger.latest_block_height();
1621
1622            // Do not issue requests for blocks already contained in the ledger.
1623            let mut start_height = ledger_height.max(sync_height + 1);
1624
1625            // Do not issue requests that already exist.
1626            while requests.contains_key(&start_height) {
1627                start_height += 1;
1628            }
1629
1630            start_height
1631        };
1632
1633        // If the minimum common ancestor is below the start height, then return early.
1634        if min_common_ancestor < start_height {
1635            if start_height < greatest_peer_height {
1636                trace!(
1637                    "No request to construct. Height for the next block request is {start_height}, but minimum common block locator ancestor is only {min_common_ancestor} (sync_height={sync_height} greatest_peer_height={greatest_peer_height})"
1638                );
1639            }
1640            return Default::default();
1641        }
1642
1643        // Compute the end height for the block request.
1644        let end_height = (min_common_ancestor + 1).min(start_height + max_blocks_to_request);
1645
1646        // Construct the block hashes to request.
1647        let mut request_hashes = IndexMap::with_capacity((start_height..end_height).len());
1648        // Track the largest number of sync IPs required for any block request in the sequence of requests.
1649        let mut max_num_sync_ips = 1;
1650
1651        for height in start_height..end_height {
1652            // Ensure the current height is not in the ledger or already requested.
1653            if self.check_block_request(height).is_err() {
1654                // If the sequence of block requests is interrupted, then return early.
1655                // Otherwise, continue until the first start height that is new.
1656                match request_hashes.is_empty() {
1657                    true => continue,
1658                    false => break,
1659                }
1660            }
1661
1662            // Construct the block request.
1663            let (hash, previous_hash, num_sync_ips, is_honest) = construct_request(height, sync_peers);
1664
1665            // Handle the dishonest case.
1666            if !is_honest {
1667                // TODO (howardwu): Consider performing an integrity check on peers (to disconnect).
1668                warn!("Detected dishonest peer(s) when preparing block request");
1669                // If there are not enough peers in the dishonest case, then return early.
1670                if sync_peers.len() < num_sync_ips {
1671                    break;
1672                }
1673            }
1674
1675            // Update the maximum number of sync IPs.
1676            max_num_sync_ips = max_num_sync_ips.max(num_sync_ips);
1677
1678            // Append the request.
1679            request_hashes.insert(height, (hash, previous_hash));
1680        }
1681
1682        // Construct the requests with the same sync ips.
1683        request_hashes
1684            .into_iter()
1685            .map(|(height, (hash, previous_hash))| (height, (hash, previous_hash, max_num_sync_ips)))
1686            .collect()
1687    }
1688}
1689
1690/// If any peer is detected to be dishonest in this function, it will not set the hash or previous hash,
1691/// in order to allow the caller to determine what to do.
1692fn construct_request<N: Network>(
1693    height: u32,
1694    sync_peers: &IndexMap<SocketAddr, BlockLocators<N>>,
1695) -> (Option<N::BlockHash>, Option<N::BlockHash>, usize, bool) {
1696    let mut hash = None;
1697    let mut hash_redundancy: usize = 0;
1698    let mut previous_hash = None;
1699    let mut is_honest = true;
1700
1701    for peer_locators in sync_peers.values() {
1702        if let Some(candidate_hash) = peer_locators.get_hash(height) {
1703            match hash {
1704                // Increment the redundancy count if the hash matches.
1705                Some(hash) if hash == candidate_hash => hash_redundancy += 1,
1706                // Some peer is dishonest.
1707                Some(_) => {
1708                    hash = None;
1709                    hash_redundancy = 0;
1710                    previous_hash = None;
1711                    is_honest = false;
1712                    break;
1713                }
1714                // Set the hash if it is not set.
1715                None => {
1716                    hash = Some(candidate_hash);
1717                    hash_redundancy = 1;
1718                }
1719            }
1720        }
1721        if let Some(candidate_previous_hash) = peer_locators.get_hash(height.saturating_sub(1)) {
1722            match previous_hash {
1723                // Increment the redundancy count if the previous hash matches.
1724                Some(previous_hash) if previous_hash == candidate_previous_hash => (),
1725                // Some peer is dishonest.
1726                Some(_) => {
1727                    hash = None;
1728                    hash_redundancy = 0;
1729                    previous_hash = None;
1730                    is_honest = false;
1731                    break;
1732                }
1733                // Set the previous hash if it is not set.
1734                None => previous_hash = Some(candidate_previous_hash),
1735            }
1736        }
1737    }
1738
1739    // Note that we intentionally do not just pick the peers that have the hash we have chosen,
1740    // to give stronger confidence that we are syncing during times when the network is consistent/stable.
1741    let num_sync_ips = {
1742        // Extra redundant peers - as the block hash was dishonest.
1743        if !is_honest {
1744            // Choose up to the extra redundancy factor in sync peers.
1745            EXTRA_REDUNDANCY_FACTOR
1746        }
1747        // No redundant peers - as we have redundancy on the block hash.
1748        else if hash.is_some() && hash_redundancy >= REDUNDANCY_FACTOR {
1749            // Choose one sync peer.
1750            1
1751        }
1752        // Redundant peers - as we do not have redundancy on the block hash.
1753        else {
1754            // Choose up to the redundancy factor in sync peers.
1755            REDUNDANCY_FACTOR
1756        }
1757    };
1758
1759    (hash, previous_hash, num_sync_ips, is_honest)
1760}
1761
1762#[cfg(test)]
1763mod tests {
1764    use super::*;
1765    use crate::locators::{
1766        CHECKPOINT_INTERVAL,
1767        NUM_RECENT_BLOCKS,
1768        test_helpers::{sample_block_locators, sample_block_locators_with_fork},
1769    };
1770
1771    use snarkos_node_bft_ledger_service::MockLedgerService;
1772    use snarkvm::{
1773        ledger::committee::Committee,
1774        prelude::{Field, TestRng},
1775    };
1776
1777    use indexmap::{IndexSet, indexset};
1778    #[cfg(feature = "locktick")]
1779    use locktick::parking_lot::RwLock;
1780    #[cfg(not(feature = "locktick"))]
1781    use parking_lot::RwLock;
1782    use rand::RngExt;
1783    use std::net::{IpAddr, Ipv4Addr};
1784
1785    type CurrentNetwork = snarkvm::prelude::MainnetV0;
1786
1787    /// Returns the peer IP for the sync pool.
1788    fn sample_peer_ip(id: u16) -> SocketAddr {
1789        assert_ne!(id, 0, "The peer ID must not be 0 (reserved for local IP in testing)");
1790        SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), id)
1791    }
1792
1793    /// Returns a sample committee.
1794    fn sample_committee() -> Committee<CurrentNetwork> {
1795        let rng = &mut TestRng::default();
1796        snarkvm::ledger::committee::test_helpers::sample_committee(rng)
1797    }
1798
1799    /// Returns the ledger service, initialized to the given height.
1800    fn sample_ledger_service(height: u32) -> MockLedgerService<CurrentNetwork> {
1801        MockLedgerService::new_at_height(sample_committee(), height)
1802    }
1803
1804    /// Returns the sync pool, with the ledger initialized to the given height.
1805    fn sample_sync_at_height(height: u32) -> BlockSync<CurrentNetwork> {
1806        BlockSync::<CurrentNetwork>::new(Arc::new(sample_ledger_service(height)), ConnectionMode::Router)
1807    }
1808
1809    /// Returns a vector of randomly sampled block heights in [0, max_height].
1810    ///
1811    /// The maximum value will always be included in the result.
1812    fn generate_block_heights(max_height: u32, num_values: usize) -> Vec<u32> {
1813        assert!(num_values > 0, "Cannot generate an empty vector");
1814        assert!((max_height as usize) >= num_values);
1815
1816        let mut rng = TestRng::default();
1817
1818        let mut heights: Vec<u32> = (0..(max_height - 1)).sample(&mut rng, num_values);
1819
1820        heights.push(max_height);
1821
1822        heights
1823    }
1824
1825    /// Returns a duplicate (deep copy) of the sync pool with a different ledger height.
1826    fn duplicate_sync_at_new_height(sync: &BlockSync<CurrentNetwork>, height: u32) -> BlockSync<CurrentNetwork> {
1827        BlockSync::<CurrentNetwork> {
1828            failed_requests: Default::default(),
1829            peer_notify: Notify::new(),
1830            response_notify: Default::default(),
1831            ledger: Arc::new(sample_ledger_service(height)),
1832            connection_mode: sync.connection_mode,
1833            locators: RwLock::new(sync.locators.read().clone()),
1834            common_ancestors: RwLock::new(sync.common_ancestors.read().clone()),
1835            requests: RwLock::new(sync.requests.read().clone()),
1836            sync_state: RwLock::new(sync.sync_state.read().clone()),
1837            synced_notify: Notify::new(),
1838            advance_with_sync_blocks_lock: Default::default(),
1839            metrics: Default::default(),
1840            prepare_requests_lock: Default::default(),
1841            last_response_at: Default::default(),
1842        }
1843    }
1844
1845    /// Checks that the sync pool (starting at genesis) returns the correct requests.
1846    fn check_prepare_block_requests(
1847        sync: BlockSync<CurrentNetwork>,
1848        min_common_ancestor: u32,
1849        peers: IndexSet<SocketAddr>,
1850    ) {
1851        let rng = &mut TestRng::default();
1852
1853        // Check test assumptions are met.
1854        assert_eq!(sync.ledger.latest_block_height(), 0, "This test assumes the sync pool is at genesis");
1855
1856        // Determine the number of peers within range of this sync pool.
1857        let num_peers_within_recent_range_of_ledger = {
1858            // If no peers are within range, then set to 0.
1859            if min_common_ancestor >= NUM_RECENT_BLOCKS as u32 {
1860                0
1861            }
1862            // Otherwise, manually check the number of peers within range.
1863            else {
1864                peers.iter().filter(|peer_ip| sync.get_peer_height(peer_ip).unwrap() < NUM_RECENT_BLOCKS as u32).count()
1865            }
1866        };
1867
1868        // Prepare the block requests.
1869        let mut batches = sync.prepare_block_requests();
1870
1871        // If there are no peers, then there should be no requests.
1872        if peers.is_empty() {
1873            assert!(batches.is_empty());
1874            return;
1875        }
1876
1877        let (requests, sync_peers) = batches.pop().unwrap();
1878
1879        // Otherwise, there should be requests.
1880        let expected_num_requests = core::cmp::min(min_common_ancestor as usize, MAX_BLOCK_REQUESTS);
1881        assert_eq!(requests.len(), expected_num_requests);
1882
1883        for (idx, (height, (hash, previous_hash, num_sync_ips))) in requests.into_iter().enumerate() {
1884            // Construct the sync IPs.
1885            let sync_ips: IndexSet<_> = sync_peers.keys().sample(rng, num_sync_ips).into_iter().copied().collect();
1886            assert_eq!(height, 1 + idx as u32);
1887            assert_eq!(hash, Some((Field::<CurrentNetwork>::from_u32(height)).into()));
1888            assert_eq!(previous_hash, Some((Field::<CurrentNetwork>::from_u32(height - 1)).into()));
1889
1890            if num_peers_within_recent_range_of_ledger >= REDUNDANCY_FACTOR {
1891                assert_eq!(sync_ips.len(), 1);
1892            } else {
1893                assert_eq!(sync_ips.len(), num_peers_within_recent_range_of_ledger);
1894                assert_eq!(sync_ips, peers);
1895            }
1896        }
1897    }
1898
1899    /// Tests that height and hash values are set correctly using many different maximum block heights.
1900    #[test]
1901    fn test_latest_block_height() {
1902        for height in generate_block_heights(100_001, 5000) {
1903            let sync = sample_sync_at_height(height);
1904            // Check that the latest block height is the maximum height.
1905            assert_eq!(sync.ledger.latest_block_height(), height);
1906
1907            // Check the hash to height mapping
1908            assert_eq!(sync.ledger.get_block_height(&(Field::<CurrentNetwork>::from_u32(0)).into()).unwrap(), 0);
1909            assert_eq!(
1910                sync.ledger.get_block_height(&(Field::<CurrentNetwork>::from_u32(height)).into()).unwrap(),
1911                height
1912            );
1913        }
1914    }
1915
1916    #[test]
1917    fn test_get_block_hash() {
1918        for height in generate_block_heights(100_001, 5000) {
1919            let sync = sample_sync_at_height(height);
1920
1921            // Check the height to hash mapping
1922            assert_eq!(sync.ledger.get_block_hash(0).unwrap(), (Field::<CurrentNetwork>::from_u32(0)).into());
1923            assert_eq!(sync.ledger.get_block_hash(height).unwrap(), (Field::<CurrentNetwork>::from_u32(height)).into());
1924        }
1925    }
1926
1927    #[test]
1928    fn test_prepare_block_requests() {
1929        for num_peers in 0..111 {
1930            println!("Testing with {num_peers} peers");
1931
1932            let sync = sample_sync_at_height(0);
1933
1934            let mut peers = indexset![];
1935
1936            for peer_id in 1..=num_peers {
1937                // Add a peer.
1938                sync.update_peer_locators(sample_peer_ip(peer_id), &sample_block_locators(10)).unwrap();
1939                // Add the peer to the set of peers.
1940                peers.insert(sample_peer_ip(peer_id));
1941            }
1942
1943            // If all peers are ahead, then requests should be prepared.
1944            check_prepare_block_requests(sync, 10, peers);
1945        }
1946    }
1947
1948    #[test]
1949    fn test_prepare_block_requests_with_leading_fork_at_11() {
1950        let sync = sample_sync_at_height(0);
1951
1952        // Intuitively, peer 1's fork is above peer 2 and peer 3's height.
1953        // So from peer 2 and peer 3's perspective, they don't even realize that peer 1 is on a fork.
1954        // Thus, you can sync up to block 10 from any of the 3 peers.
1955
1956        // When there are NUM_REDUNDANCY peers ahead, and 1 peer is on a leading fork at 11,
1957        // then the sync pool should request blocks 1..=10 from the NUM_REDUNDANCY peers.
1958        // This is safe because the leading fork is at 11, and the sync pool is at 0,
1959        // so all candidate peers are at least 10 blocks ahead of the sync pool.
1960
1961        // Add a peer (fork).
1962        let peer_1 = sample_peer_ip(1);
1963        sync.update_peer_locators(peer_1, &sample_block_locators_with_fork(20, 11)).unwrap();
1964
1965        // Add a peer.
1966        let peer_2 = sample_peer_ip(2);
1967        sync.update_peer_locators(peer_2, &sample_block_locators(10)).unwrap();
1968
1969        // Add a peer.
1970        let peer_3 = sample_peer_ip(3);
1971        sync.update_peer_locators(peer_3, &sample_block_locators(10)).unwrap();
1972
1973        // Prepare the block requests.
1974        let (requests, _) = sync.prepare_block_requests().pop().unwrap();
1975        assert_eq!(requests.len(), 10);
1976
1977        // Check the requests.
1978        for (idx, (height, (hash, previous_hash, num_sync_ips))) in requests.into_iter().enumerate() {
1979            assert_eq!(height, 1 + idx as u32);
1980            assert_eq!(hash, Some((Field::<CurrentNetwork>::from_u32(height)).into()));
1981            assert_eq!(previous_hash, Some((Field::<CurrentNetwork>::from_u32(height - 1)).into()));
1982            assert_eq!(num_sync_ips, 1); // Only 1 needed since we have redundancy factor on this (recent locator) hash.
1983        }
1984    }
1985
1986    #[test]
1987    fn test_prepare_block_requests_with_leading_fork_at_10() {
1988        let rng = &mut TestRng::default();
1989        let sync = sample_sync_at_height(0);
1990
1991        // Intuitively, peer 1's fork is at peer 2 and peer 3's height.
1992        // So from peer 2 and peer 3's perspective, they recognize that peer 1 has forked.
1993        // Thus, you don't have NUM_REDUNDANCY peers to sync to block 10.
1994        //
1995        // Now, while you could in theory sync up to block 9 from any of the 3 peers,
1996        // we choose not to do this as either side is likely to disconnect from us,
1997        // and we would rather wait for enough redundant peers before syncing.
1998
1999        // When there are NUM_REDUNDANCY peers ahead, and 1 peer is on a leading fork at 10,
2000        // then the sync pool should not request blocks as 1 peer conflicts with the other NUM_REDUNDANCY-1 peers.
2001        // We choose to sync with a cohort of peers that are *consistent* with each other,
2002        // and prioritize from descending heights (so the highest peer gets priority).
2003
2004        // Add a peer (fork).
2005        let peer_1 = sample_peer_ip(1);
2006        sync.update_peer_locators(peer_1, &sample_block_locators_with_fork(20, 10)).unwrap();
2007
2008        // Add a peer.
2009        let peer_2 = sample_peer_ip(2);
2010        sync.update_peer_locators(peer_2, &sample_block_locators(10)).unwrap();
2011
2012        // Add a peer.
2013        let peer_3 = sample_peer_ip(3);
2014        sync.update_peer_locators(peer_3, &sample_block_locators(10)).unwrap();
2015
2016        // Prepare the block requests.
2017        let batches = sync.prepare_block_requests();
2018        assert!(batches.is_empty());
2019
2020        // When there are NUM_REDUNDANCY+1 peers ahead, and 1 is on a fork, then there should be block requests.
2021
2022        // Add a peer.
2023        let peer_4 = sample_peer_ip(4);
2024        sync.update_peer_locators(peer_4, &sample_block_locators(10)).unwrap();
2025
2026        // Prepare the block requests.
2027        let (requests, sync_peers) = sync.prepare_block_requests().pop().unwrap();
2028        assert_eq!(requests.len(), 10);
2029
2030        // Check the requests.
2031        for (idx, (height, (hash, previous_hash, num_sync_ips))) in requests.into_iter().enumerate() {
2032            // Construct the sync IPs.
2033            let sync_ips: IndexSet<_> = sync_peers.keys().sample(rng, num_sync_ips).into_iter().copied().collect();
2034            assert_eq!(height, 1 + idx as u32);
2035            assert_eq!(hash, Some((Field::<CurrentNetwork>::from_u32(height)).into()));
2036            assert_eq!(previous_hash, Some((Field::<CurrentNetwork>::from_u32(height - 1)).into()));
2037            assert_eq!(sync_ips.len(), 1); // Only 1 needed since we have redundancy factor on this (recent locator) hash.
2038            assert_ne!(sync_ips[0], peer_1); // It should never be the forked peer.
2039        }
2040    }
2041
2042    #[test]
2043    fn test_prepare_block_requests_with_trailing_fork_at_9() {
2044        let rng = &mut TestRng::default();
2045        let sync = sample_sync_at_height(0);
2046
2047        // Peer 1 and 2 diverge from peer 3 at block 10. We only sync when there are NUM_REDUNDANCY peers
2048        // who are *consistent* with each other. So if you add a 4th peer that is consistent with peer 1 and 2,
2049        // then you should be able to sync up to block 10, thereby biasing away from peer 3.
2050
2051        // Add a peer (fork).
2052        let peer_1 = sample_peer_ip(1);
2053        sync.update_peer_locators(peer_1, &sample_block_locators(10)).unwrap();
2054
2055        // Add a peer.
2056        let peer_2 = sample_peer_ip(2);
2057        sync.update_peer_locators(peer_2, &sample_block_locators(10)).unwrap();
2058
2059        // Add a peer.
2060        let peer_3 = sample_peer_ip(3);
2061        sync.update_peer_locators(peer_3, &sample_block_locators_with_fork(20, 10)).unwrap();
2062
2063        // Prepare the block requests.
2064        let batches = sync.prepare_block_requests();
2065        assert!(batches.is_empty());
2066
2067        // When there are NUM_REDUNDANCY+1 peers ahead, and peer 3 is on a fork, then there should be block requests.
2068
2069        // Add a peer.
2070        let peer_4 = sample_peer_ip(4);
2071        sync.update_peer_locators(peer_4, &sample_block_locators(10)).unwrap();
2072
2073        // Prepare the block requests.
2074        let (requests, sync_peers) = sync.prepare_block_requests().pop().unwrap();
2075        assert_eq!(requests.len(), 10);
2076
2077        // Check the requests.
2078        for (idx, (height, (hash, previous_hash, num_sync_ips))) in requests.into_iter().enumerate() {
2079            // Construct the sync IPs.
2080            let sync_ips: IndexSet<_> = sync_peers.keys().sample(rng, num_sync_ips).into_iter().copied().collect();
2081            assert_eq!(height, 1 + idx as u32);
2082            assert_eq!(hash, Some((Field::<CurrentNetwork>::from_u32(height)).into()));
2083            assert_eq!(previous_hash, Some((Field::<CurrentNetwork>::from_u32(height - 1)).into()));
2084            assert_eq!(sync_ips.len(), 1); // Only 1 needed since we have redundancy factor on this (recent locator) hash.
2085            assert_ne!(sync_ips[0], peer_3); // It should never be the forked peer.
2086        }
2087    }
2088
2089    #[test]
2090    fn test_insert_block_requests() {
2091        let rng = &mut TestRng::default();
2092        let sync = sample_sync_at_height(0);
2093
2094        // Add a peer.
2095        sync.update_peer_locators(sample_peer_ip(1), &sample_block_locators(10)).unwrap();
2096
2097        // Prepare the block requests.
2098        let (requests, sync_peers) = sync.prepare_block_requests().pop().unwrap();
2099        assert_eq!(requests.len(), 10);
2100
2101        for (height, (hash, previous_hash, num_sync_ips)) in requests.clone() {
2102            // Construct the sync IPs.
2103            let sync_ips: IndexSet<_> = sync_peers.keys().sample(rng, num_sync_ips).into_iter().copied().collect();
2104            // Insert the block request.
2105            sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap();
2106            // Check that the block requests were inserted.
2107            assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips)));
2108            assert!(sync.get_block_request_timestamp(height).is_some());
2109        }
2110
2111        for (height, (hash, previous_hash, num_sync_ips)) in requests.clone() {
2112            // Construct the sync IPs.
2113            let sync_ips: IndexSet<_> = sync_peers.keys().sample(rng, num_sync_ips).into_iter().copied().collect();
2114            // Check that the block requests are still inserted.
2115            assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips)));
2116            assert!(sync.get_block_request_timestamp(height).is_some());
2117        }
2118
2119        for (height, (hash, previous_hash, num_sync_ips)) in requests {
2120            // Construct the sync IPs.
2121            let sync_ips: IndexSet<_> = sync_peers.keys().sample(rng, num_sync_ips).into_iter().copied().collect();
2122            // Ensure that the block requests cannot be inserted twice.
2123            sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap_err();
2124            // Check that the block requests are still inserted.
2125            assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips)));
2126            assert!(sync.get_block_request_timestamp(height).is_some());
2127        }
2128    }
2129
2130    #[test]
2131    fn test_insert_block_requests_fails() {
2132        let sync = sample_sync_at_height(9);
2133
2134        // Add a peer.
2135        sync.update_peer_locators(sample_peer_ip(1), &sample_block_locators(10)).unwrap();
2136
2137        // Inserting a block height that is already in the ledger should fail.
2138        sync.insert_block_request(9, (None, None, indexset![sample_peer_ip(1)])).unwrap_err();
2139        // Inserting a block height that is not in the ledger should succeed.
2140        sync.insert_block_request(10, (None, None, indexset![sample_peer_ip(1)])).unwrap();
2141    }
2142
2143    #[test]
2144    fn test_update_peer_locators() {
2145        let sync = sample_sync_at_height(0);
2146
2147        // Test 2 peers.
2148        let peer1_ip = sample_peer_ip(1);
2149        for peer1_height in 0..500u32 {
2150            sync.update_peer_locators(peer1_ip, &sample_block_locators(peer1_height)).unwrap();
2151            assert_eq!(sync.get_peer_height(&peer1_ip), Some(peer1_height));
2152
2153            let peer2_ip = sample_peer_ip(2);
2154            for peer2_height in 0..500u32 {
2155                println!("Testing peer 1 height at {peer1_height} and peer 2 height at {peer2_height}");
2156
2157                sync.update_peer_locators(peer2_ip, &sample_block_locators(peer2_height)).unwrap();
2158                assert_eq!(sync.get_peer_height(&peer2_ip), Some(peer2_height));
2159
2160                // Compute the distance between the peers.
2161                let distance = peer1_height.abs_diff(peer2_height);
2162
2163                // Check the common ancestor.
2164                if distance < NUM_RECENT_BLOCKS as u32 {
2165                    let expected_ancestor = core::cmp::min(peer1_height, peer2_height);
2166                    assert_eq!(sync.get_common_ancestor(peer1_ip, peer2_ip), Some(expected_ancestor));
2167                    assert_eq!(sync.get_common_ancestor(peer2_ip, peer1_ip), Some(expected_ancestor));
2168                } else {
2169                    let min_checkpoints =
2170                        core::cmp::min(peer1_height / CHECKPOINT_INTERVAL, peer2_height / CHECKPOINT_INTERVAL);
2171                    let expected_ancestor = min_checkpoints * CHECKPOINT_INTERVAL;
2172                    assert_eq!(sync.get_common_ancestor(peer1_ip, peer2_ip), Some(expected_ancestor));
2173                    assert_eq!(sync.get_common_ancestor(peer2_ip, peer1_ip), Some(expected_ancestor));
2174                }
2175            }
2176        }
2177    }
2178
2179    #[test]
2180    fn test_remove_peer() {
2181        let sync = sample_sync_at_height(0);
2182
2183        let peer_ip = sample_peer_ip(1);
2184        sync.update_peer_locators(peer_ip, &sample_block_locators(100)).unwrap();
2185        assert_eq!(sync.get_peer_height(&peer_ip), Some(100));
2186
2187        sync.remove_peer(&peer_ip);
2188        assert_eq!(sync.get_peer_height(&peer_ip), None);
2189
2190        sync.update_peer_locators(peer_ip, &sample_block_locators(200)).unwrap();
2191        assert_eq!(sync.get_peer_height(&peer_ip), Some(200));
2192
2193        sync.remove_peer(&peer_ip);
2194        assert_eq!(sync.get_peer_height(&peer_ip), None);
2195    }
2196
2197    #[test]
2198    fn test_locators_insert_remove_insert() {
2199        let sync = sample_sync_at_height(0);
2200
2201        let peer_ip = sample_peer_ip(1);
2202        sync.update_peer_locators(peer_ip, &sample_block_locators(100)).unwrap();
2203        assert_eq!(sync.get_peer_height(&peer_ip), Some(100));
2204
2205        sync.remove_peer(&peer_ip);
2206        assert_eq!(sync.get_peer_height(&peer_ip), None);
2207
2208        sync.update_peer_locators(peer_ip, &sample_block_locators(200)).unwrap();
2209        assert_eq!(sync.get_peer_height(&peer_ip), Some(200));
2210    }
2211
2212    #[test]
2213    fn test_requests_insert_remove_insert() {
2214        let rng = &mut TestRng::default();
2215        let sync = sample_sync_at_height(0);
2216
2217        // Add a peer.
2218        let peer_ip = sample_peer_ip(1);
2219        sync.update_peer_locators(peer_ip, &sample_block_locators(10)).unwrap();
2220
2221        // Prepare the block requests.
2222        let (requests, sync_peers) = sync.prepare_block_requests().pop().unwrap();
2223        assert_eq!(requests.len(), 10);
2224
2225        for (height, (hash, previous_hash, num_sync_ips)) in requests.clone() {
2226            // Construct the sync IPs.
2227            let sync_ips: IndexSet<_> = sync_peers.keys().sample(rng, num_sync_ips).into_iter().copied().collect();
2228            // Insert the block request.
2229            sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap();
2230            // Check that the block requests were inserted.
2231            assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips)));
2232            assert!(sync.get_block_request_timestamp(height).is_some());
2233        }
2234
2235        // Remove the peer.
2236        sync.remove_peer(&peer_ip);
2237
2238        for (height, _) in requests {
2239            // Check that the block requests were removed.
2240            assert_eq!(sync.get_block_request(height), None);
2241            assert!(sync.get_block_request_timestamp(height).is_none());
2242        }
2243
2244        // As there is no peer, it should not be possible to prepare block requests.
2245        let batches = sync.prepare_block_requests();
2246        assert!(batches.is_empty());
2247
2248        // Add the peer again.
2249        sync.update_peer_locators(peer_ip, &sample_block_locators(10)).unwrap();
2250
2251        // Prepare the block requests.
2252        let (requests, _) = sync.prepare_block_requests().pop().unwrap();
2253        assert_eq!(requests.len(), 10);
2254
2255        for (height, (hash, previous_hash, num_sync_ips)) in requests {
2256            // Construct the sync IPs.
2257            let sync_ips: IndexSet<_> = sync_peers.keys().sample(rng, num_sync_ips).into_iter().copied().collect();
2258            // Insert the block request.
2259            sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap();
2260            // Check that the block requests were inserted.
2261            assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips)));
2262            assert!(sync.get_block_request_timestamp(height).is_some());
2263        }
2264    }
2265
2266    #[test]
2267    fn test_obsolete_block_requests() {
2268        let rng = &mut TestRng::default();
2269        let sync = sample_sync_at_height(0);
2270
2271        // Ensure the locator always includes at least one block,
2272        // that is not the genesis block.
2273        // Otherwise there are no block requests to construct.
2274        let locator_height = rng.random_range(1..50);
2275
2276        // Add a peer.
2277        let locators = sample_block_locators(locator_height);
2278        sync.update_peer_locators(sample_peer_ip(1), &locators).unwrap();
2279
2280        // Construct block requests
2281        let (requests, sync_peers) = sync.prepare_block_requests().pop().unwrap();
2282        assert_eq!(requests.len(), locator_height as usize);
2283
2284        // Add the block requests to the sync module.
2285        for (height, (hash, previous_hash, num_sync_ips)) in requests.clone() {
2286            // Construct the sync IPs.
2287            let sync_ips: IndexSet<_> = sync_peers.keys().sample(rng, num_sync_ips).into_iter().copied().collect();
2288            // Insert the block request.
2289            sync.insert_block_request(height, (hash, previous_hash, sync_ips.clone())).unwrap();
2290            // Check that the block requests were inserted.
2291            assert_eq!(sync.get_block_request(height), Some((hash, previous_hash, sync_ips)));
2292            assert!(sync.get_block_request_timestamp(height).is_some());
2293        }
2294
2295        // Duplicate a new sync module with a different height to simulate block advancement.
2296        // This range needs to be inclusive, so that the range is never empty,
2297        // even with a locator height of 0.
2298        let ledger_height = rng.random_range(0..=locator_height);
2299        let new_sync = duplicate_sync_at_new_height(&sync, ledger_height);
2300
2301        // Check that the number of requests is the same.
2302        assert_eq!(new_sync.requests.read().len(), requests.len());
2303
2304        // Remove timed out block requests.
2305        new_sync.handle_block_request_timeouts();
2306
2307        // Check that the number of requests is reduced based on the ledger height.
2308        assert_eq!(new_sync.requests.read().len(), (locator_height - ledger_height) as usize);
2309    }
2310
2311    #[test]
2312    fn test_timed_out_block_request() {
2313        let sync = sample_sync_at_height(0);
2314        let peer_ip = sample_peer_ip(1);
2315        let locators = sample_block_locators(10);
2316        let block_hash = locators.get_hash(1);
2317
2318        sync.update_peer_locators(peer_ip, &locators).unwrap();
2319
2320        let timestamp = Instant::now() - BLOCK_REQUEST_TIMEOUT - Duration::from_secs(1);
2321
2322        // Add a timed-out request
2323        sync.requests.write().insert(1, OutstandingRequest {
2324            request: (block_hash, None, [peer_ip].into()),
2325            timestamp,
2326            response: None,
2327        });
2328
2329        assert_eq!(sync.requests.read().len(), 1);
2330        assert_eq!(sync.locators.read().len(), 1);
2331
2332        // Remove timed out block requests.
2333        sync.handle_block_request_timeouts();
2334
2335        // let ban_list = c.peers_to_ban.write();
2336        // assert_eq!(ban_list.len(), 1);
2337        // assert_eq!(ban_list.iter().next(), Some(&peer_ip));
2338
2339        assert!(sync.requests.read().is_empty());
2340        assert!(sync.locators.read().is_empty());
2341    }
2342
2343    #[test]
2344    fn test_reissue_timed_out_block_request() {
2345        let sync = sample_sync_at_height(0);
2346        let peer_ip1 = sample_peer_ip(1);
2347        let peer_ip2 = sample_peer_ip(2);
2348        let peer_ip3 = sample_peer_ip(3);
2349
2350        let locators = sample_block_locators(10);
2351        let block_hash1 = locators.get_hash(1);
2352        let block_hash2 = locators.get_hash(2);
2353
2354        sync.update_peer_locators(peer_ip1, &locators).unwrap();
2355        sync.update_peer_locators(peer_ip2, &locators).unwrap();
2356        sync.update_peer_locators(peer_ip3, &locators).unwrap();
2357
2358        assert_eq!(sync.locators.read().len(), 3);
2359
2360        let timestamp = Instant::now() - BLOCK_REQUEST_TIMEOUT - Duration::from_secs(1);
2361
2362        // Add a timed-out request
2363        sync.requests.write().insert(1, OutstandingRequest {
2364            request: (block_hash1, None, [peer_ip1].into()),
2365            timestamp,
2366            response: None,
2367        });
2368
2369        // Add a timed-out request
2370        sync.requests.write().insert(2, OutstandingRequest {
2371            request: (block_hash2, None, [peer_ip2].into()),
2372            timestamp: Instant::now(),
2373            response: None,
2374        });
2375
2376        assert_eq!(sync.requests.read().len(), 2);
2377
2378        // Remove timed out block requests.
2379        sync.handle_block_request_timeouts();
2380
2381        // let ban_list = c.peers_to_ban.write();
2382        // assert_eq!(ban_list.len(), 1);
2383        // assert_eq!(ban_list.iter().next(), Some(&peer_ip1));
2384
2385        assert_eq!(sync.requests.read().len(), 1);
2386        assert_eq!(sync.locators.read().len(), 2);
2387
2388        let failed_requests = sync.failed_requests.lock();
2389        assert_eq!(failed_requests.len(), 1);
2390
2391        let (height, (hash, _)) = failed_requests.iter().next().unwrap();
2392        assert_eq!(*height, 1);
2393        assert_eq!(*hash, block_hash1);
2394        /*
2395        assert_eq!(new_sync_ips.len(), 2);
2396
2397        // Make sure the removed peer is not in the sync_peer set.
2398        let mut iter = new_sync_ips.iter();
2399        assert_ne!(iter.next().unwrap().0, &peer_ip1);
2400        assert_ne!(iter.next().unwrap().0, &peer_ip1);*/
2401    }
2402}