snarkos_node_sync_locators/
block_locators.rs

1// Copyright (c) 2019-2025 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 snarkvm::prelude::{FromBytes, IoResult, Network, Read, ToBytes, Write, error, has_duplicates};
17
18use anyhow::{Result, bail, ensure};
19use indexmap::{IndexMap, indexmap};
20use serde::{Deserialize, Serialize};
21use std::collections::{BTreeMap, btree_map::IntoIter};
22
23/// The number of recent blocks (near tip).
24pub const NUM_RECENT_BLOCKS: usize = 100; // 100 blocks
25/// The interval between recent blocks.
26const RECENT_INTERVAL: u32 = 1; // 1 block intervals
27/// The interval between block checkpoints.
28pub const CHECKPOINT_INTERVAL: u32 = 10_000; // 10,000 block intervals
29// The maximum number of checkpoints that there can be
30const MAX_CHECKPOINTS: usize = (u32::MAX / CHECKPOINT_INTERVAL) as usize;
31
32/// Block locator maps.
33///
34/// This data structure is used by validators to advertise the blocks that
35/// they have and can provide to other validators to help them sync.
36/// Periodically, each validator broadcasts a [`PrimaryPing`],
37/// which contains a `BlockLocators` instance.
38/// Recall that blocks are indexed by their `u32` height, starting with 0 for the genesis block.
39/// The keys of the `recents` and `checkpoints` maps are the block heights;
40/// the values of the maps are the corresponding block hashes.
41///
42/// If a validator has `N` blocks, the `recents` and `checkpoints` maps are as follows:
43/// - The `recents` map contains entries for blocks at heights
44///   `N - 1 - (NUM_RECENT_BLOCKS - 1) * RECENT_INTERVAL`,
45///   `N - 1 - (NUM_RECENT_BLOCKS - 2) * RECENT_INTERVAL`,
46///   ...,
47///   `N - 1`.
48///   If any of the just listed heights are negative, there are no entries for them of course,
49///   and the `recents` map has fewer than `NUM_RECENT_BLOCKS` entries.
50///   If `RECENT_INTERVAL` is 1, the `recents` map contains entries
51///   for the last `NUM_RECENT_BLOCKS` blocks, i.e. from `N - NUM_RECENT_BLOCKS` to `N - 1`;
52///   if additionally `N < NUM_RECENT_BLOCKS`, the `recents` map contains
53///   entries for all the blocks, from `0` to `N - 1`.
54/// - The `checkpoints` map contains an entry for every `CHECKPOINT_INTERVAL`-th block,
55///   starting with 0 and not exceeding `N`, i.e. it has entries for blocks
56///   `0`, `CHECKPOINT_INTERVAL`, `2 * CHECKPOINT_INTERVAL`, ..., `k * CHECKPOINT_INTERVAL`,
57///   where `k` is the maximum integer such that `k * CHECKPOINT_INTERVAL <= N`.
58///
59/// The `recents` and `checkpoints` maps may have overlapping entries,
60/// e.g. if `N-1` is a multiple of `CHECKPOINT_INTERVAL`;
61/// but if `CHECKPOINT_INTERVAL` is much larger than `NUM_RECENT_BLOCKS`,
62/// there is no overlap most of the time.
63///
64/// We call `BlockLocators` with the form described above 'well-formed'.
65///
66/// Well-formed `BlockLocators` instances are built by [`BlockSync::get_block_locators()`].
67/// When a `BlockLocators` instance is received (in a [`PrimaryPing`]) by a validator,
68/// the maps may not be well-formed (if the sending validator is faulty),
69/// but the receiving validator ensures that they are well-formed
70/// by calling [`BlockLocators::ensure_is_valid()`] from [`BlockLocators::new()`],
71/// when deserializing in [`BlockLocators::read_le()`].
72/// So this well-formedness is an invariant of `BlockLocators` instances.
73#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
74pub struct BlockLocators<N: Network> {
75    /// The map of recent blocks.
76    pub recents: IndexMap<u32, N::BlockHash>,
77    /// The map of block checkpoints.
78    pub checkpoints: IndexMap<u32, N::BlockHash>,
79}
80
81impl<N: Network> BlockLocators<N> {
82    /// Initializes a new instance of the block locators, checking the validity of the block locators.
83    pub fn new(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Result<Self> {
84        // Construct the block locators.
85        let locators = Self { recents, checkpoints };
86        // Ensure the block locators are well-formed.
87        locators.ensure_is_valid()?;
88        // Return the block locators.
89        Ok(locators)
90    }
91
92    /// Initializes a new instance of the block locators, without checking the validity of the block locators.
93    /// This is only used for testing; note that it is non-public.
94    #[cfg(test)]
95    fn new_unchecked(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Self {
96        Self { recents, checkpoints }
97    }
98
99    /// Initializes a new genesis instance of the block locators.
100    pub fn new_genesis(genesis_hash: N::BlockHash) -> Self {
101        Self { recents: indexmap![0 => genesis_hash], checkpoints: indexmap![0 => genesis_hash] }
102    }
103}
104
105impl<N: Network> IntoIterator for BlockLocators<N> {
106    type IntoIter = IntoIter<u32, N::BlockHash>;
107    type Item = (u32, N::BlockHash);
108
109    // TODO (howardwu): Consider using `BTreeMap::from_par_iter` if it is more performant.
110    //  Check by sorting 300-1000 items and comparing the performance.
111    //  (https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html#method.from_par_iter)
112    fn into_iter(self) -> Self::IntoIter {
113        BTreeMap::from_iter(self.checkpoints.into_iter().chain(self.recents)).into_iter()
114    }
115}
116
117impl<N: Network> BlockLocators<N> {
118    /// Returns the latest locator height.
119    pub fn latest_locator_height(&self) -> u32 {
120        self.recents.keys().last().copied().unwrap_or_default()
121    }
122
123    /// Returns the block hash for the given block height, if it exists.
124    pub fn get_hash(&self, height: u32) -> Option<N::BlockHash> {
125        self.recents.get(&height).copied().or_else(|| self.checkpoints.get(&height).copied())
126    }
127
128    /// Returns `true` if the block locators are well-formed.
129    pub fn is_valid(&self) -> bool {
130        // Ensure the block locators are well-formed.
131        if let Err(error) = self.ensure_is_valid() {
132            warn!("Block locators are invalid: {error}");
133            return false;
134        }
135        true
136    }
137
138    /// Returns `true` if the given block locators are consistent with this one.
139    /// This function assumes the given block locators are well-formed.
140    pub fn is_consistent_with(&self, other: &Self) -> bool {
141        // Ensure the block locators are consistent with the previous ones.
142        if let Err(error) = self.ensure_is_consistent_with(other) {
143            warn!("Inconsistent block locators: {error}");
144            return false;
145        }
146        true
147    }
148
149    /// Checks that this block locators instance is well-formed.
150    pub fn ensure_is_valid(&self) -> Result<()> {
151        // Ensure the block locators are well-formed.
152        Self::check_block_locators(&self.recents, &self.checkpoints)
153    }
154
155    /// Returns `true` if the given block locators are consistent with this one.
156    /// This function assumes the given block locators are well-formed.
157    pub fn ensure_is_consistent_with(&self, other: &Self) -> Result<()> {
158        Self::check_consistent_block_locators(self, other)
159    }
160}
161
162impl<N: Network> BlockLocators<N> {
163    /// Checks the old and new block locators share a consistent view of block history.
164    /// This function assumes the given block locators are well-formed.
165    pub fn check_consistent_block_locators(
166        old_locators: &BlockLocators<N>,
167        new_locators: &BlockLocators<N>,
168    ) -> Result<()> {
169        // For the overlapping recent blocks, ensure their block hashes match.
170        for (height, hash) in new_locators.recents.iter() {
171            if let Some(recent_hash) = old_locators.recents.get(height) {
172                if recent_hash != hash {
173                    bail!("Recent block hash mismatch at height {height}")
174                }
175            }
176        }
177        // For the overlapping block checkpoints, ensure their block hashes match.
178        for (height, hash) in new_locators.checkpoints.iter() {
179            if let Some(checkpoint_hash) = old_locators.checkpoints.get(height) {
180                if checkpoint_hash != hash {
181                    bail!("Block checkpoint hash mismatch for height {height}")
182                }
183            }
184        }
185        Ok(())
186    }
187
188    /// Checks that the block locators are well-formed.
189    pub fn check_block_locators(
190        recents: &IndexMap<u32, N::BlockHash>,
191        checkpoints: &IndexMap<u32, N::BlockHash>,
192    ) -> Result<()> {
193        // Ensure the recent blocks are well-formed.
194        let last_recent_height = Self::check_recent_blocks(recents)?;
195        // Ensure the block checkpoints are well-formed.
196        let last_checkpoint_height = Self::check_block_checkpoints(checkpoints)?;
197
198        // Ensure that `last_checkpoint_height` is
199        // the largest multiple of `CHECKPOINT_INTERVAL` that does not exceed `last_recent_height`.
200        // That is, we must have
201        // `last_checkpoint_height <= last_recent_height < last_checkpoint_height + CHECKPOINT_INTERVAL`.
202        // Although we do not expect to run out of `u32` for block heights,
203        // `last_checkpoint_height` is an untrusted value that may come from a faulty validator,
204        // and thus we use a saturating addition;
205        // only a faulty validator would send block locators with such high block heights,
206        // under the assumption that the blockchain is always well below the `u32` limit for heights.
207        if !(last_checkpoint_height..last_checkpoint_height.saturating_add(CHECKPOINT_INTERVAL))
208            .contains(&last_recent_height)
209        {
210            bail!(
211                "Last checkpoint height ({last_checkpoint_height}) is not the largest multiple of \
212                 {CHECKPOINT_INTERVAL} that does not exceed the last recent height ({last_recent_height})"
213            )
214        }
215
216        // Ensure that if the recents and checkpoints maps overlap, they agree on the hash:
217        // we calculate the distance from the last recent to the last checkpoint;
218        // if that distance is `NUM_RECENT_BLOCKS` or more, there is no overlap;
219        // otherwise, the overlap is at the last checkpoint,
220        // which is exactly at the last recent height minus its distance from the last checkpoint.
221        // All of this also works if the last checkpoint is 0:
222        // in this case, there is an overlap (at 0) exactly when the last recent height,
223        // which is the same as its distance from the last checkpoint (0),
224        // is less than `NUM_RECENT_BLOCKS`.
225        // All of this only works if `NUM_RECENT_BLOCKS < CHECKPOINT_INTERVAL`,
226        // because it is only under this condition that there is at most one overlapping height.
227        // TODO: generalize check for RECENT_INTERVAL > 1, or remove this comment if we hardwire that to 1
228        let last_recent_to_last_checkpoint_distance = last_recent_height % CHECKPOINT_INTERVAL;
229        if last_recent_to_last_checkpoint_distance < NUM_RECENT_BLOCKS as u32 {
230            let common = last_recent_height - last_recent_to_last_checkpoint_distance;
231            if recents.get(&common).unwrap() != checkpoints.get(&common).unwrap() {
232                bail!("Recent block hash and checkpoint hash mismatch at height {common}")
233            }
234        }
235
236        Ok(())
237    }
238
239    /// Checks the recent blocks, returning the last block height from the map.
240    ///
241    /// This function checks the following:
242    /// 1. The map is not empty.
243    /// 2. The map is at the correct interval.
244    /// 3. The map is at the correct height.
245    /// 4. The map is in the correct order.
246    /// 5. The map does not contain too many entries.
247    fn check_recent_blocks(recents: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
248        // Ensure the number of recent blocks is at least 1.
249        if recents.is_empty() {
250            bail!("There must be at least 1 recent block")
251        }
252        // Ensure the number of recent blocks is at most NUM_RECENT_BLOCKS.
253        // This redundant check ensures we early exit if the number of recent blocks is too large.
254        if recents.len() > NUM_RECENT_BLOCKS {
255            bail!("There can be at most {NUM_RECENT_BLOCKS} blocks in the map")
256        }
257
258        // Ensure the given recent blocks increment in height, and at the correct interval.
259        let mut last_height = 0;
260        for (i, current_height) in recents.keys().enumerate() {
261            if i == 0 && recents.len() < NUM_RECENT_BLOCKS && *current_height > 0 {
262                bail!("Ledgers under {NUM_RECENT_BLOCKS} blocks must have the first recent block at height 0")
263            }
264            if i > 0 && *current_height <= last_height {
265                bail!("Recent blocks must increment in height")
266            }
267            if i > 0 && *current_height - last_height != RECENT_INTERVAL {
268                bail!("Recent blocks must increment by {RECENT_INTERVAL}")
269            }
270            last_height = *current_height;
271        }
272
273        // At this point, if last_height < NUM_RECENT_BLOCKS`,
274        // we know that the `recents` map consists of exactly block heights from 0 to last_height,
275        // because the loop above has ensured that the first entry is for height 0,
276        // and at the end of the loop `last_height` is the last key in `recents`,
277        // and all the keys in `recents` are consecutive in increments of 1.
278        // So the `recents` map consists of NUM_RECENT_BLOCKS or fewer entries.
279
280        // If last height >= NUM_RECENT_BLOCKS, ensure the number of recent blocks matches NUM_RECENT_BLOCKS.
281        // TODO: generalize check for RECENT_INTERVAL > 1, or remove this comment if we hardwire that to 1
282        if last_height >= NUM_RECENT_BLOCKS as u32 && recents.len() != NUM_RECENT_BLOCKS {
283            bail!("Number of recent blocks must match {NUM_RECENT_BLOCKS}")
284        }
285
286        // Ensure the block hashes are unique.
287        if has_duplicates(recents.values()) {
288            bail!("Recent block hashes must be unique")
289        }
290
291        Ok(last_height)
292    }
293
294    /// Checks the block checkpoints, returning the last block height from the checkpoints.
295    ///
296    /// This function checks the following:
297    /// 1. The block checkpoints are not empty.
298    /// 2. The block checkpoints are at the correct interval.
299    /// 3. The block checkpoints are at the correct height.
300    /// 4. The block checkpoints are in the correct order.
301    fn check_block_checkpoints(checkpoints: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
302        // Ensure the block checkpoints are not empty.
303        ensure!(!checkpoints.is_empty(), "There must be at least 1 block checkpoint");
304
305        // Ensure the given checkpoints increment in height, and at the correct interval.
306        let mut last_height = 0;
307        for (i, current_height) in checkpoints.keys().enumerate() {
308            if i == 0 && *current_height != 0 {
309                bail!("First block checkpoint must be at height 0")
310            }
311            if i > 0 && *current_height <= last_height {
312                bail!("Block checkpoints must increment in height")
313            }
314            if i > 0 && *current_height - last_height != CHECKPOINT_INTERVAL {
315                bail!("Block checkpoints must increment by {CHECKPOINT_INTERVAL}")
316            }
317            last_height = *current_height;
318        }
319
320        // Ensure the block hashes are unique.
321        if has_duplicates(checkpoints.values()) {
322            bail!("Block checkpoints must be unique")
323        }
324
325        Ok(last_height)
326    }
327}
328
329impl<N: Network> FromBytes for BlockLocators<N> {
330    fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
331        // Read the number of recent block hashes.
332        let num_recents = u32::read_le(&mut reader)?;
333        // Ensure the number of recent blocks is within bounds
334        if num_recents as usize > NUM_RECENT_BLOCKS {
335            return Err(error(format!(
336                "Number of recent blocks ({num_recents}) is greater than the maximum ({NUM_RECENT_BLOCKS})"
337            )));
338        }
339        // Read the recent block hashes.
340        let mut recents = IndexMap::with_capacity(num_recents as usize);
341        for _ in 0..num_recents {
342            let height = u32::read_le(&mut reader)?;
343            let hash = N::BlockHash::read_le(&mut reader)?;
344            recents.insert(height, hash);
345        }
346
347        // Read the number of checkpoints.
348        let num_checkpoints = u32::read_le(&mut reader)?;
349        // Ensure the number of checkpoints is within bounds
350        if num_checkpoints as usize > MAX_CHECKPOINTS {
351            return Err(error(format!(
352                "Number of checkpoints ({num_checkpoints}) is greater than the maximum ({MAX_CHECKPOINTS})"
353            )));
354        }
355        // Read the checkpoints.
356        let mut checkpoints = IndexMap::new();
357        for _ in 0..num_checkpoints {
358            let height = u32::read_le(&mut reader)?;
359            let hash = N::BlockHash::read_le(&mut reader)?;
360            checkpoints.insert(height, hash);
361        }
362
363        Self::new(recents, checkpoints).map_err(error)
364    }
365}
366
367impl<N: Network> ToBytes for BlockLocators<N> {
368    fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
369        // Write the number of recent block hashes.
370        u32::try_from(self.recents.len()).map_err(error)?.write_le(&mut writer)?;
371        // Write the recent block hashes.
372        for (height, hash) in &self.recents {
373            height.write_le(&mut writer)?;
374            hash.write_le(&mut writer)?;
375        }
376
377        // Write the number of checkpoints.
378        u32::try_from(self.checkpoints.len()).map_err(error)?.write_le(&mut writer)?;
379        // Write the checkpoints.
380        for (height, hash) in &self.checkpoints {
381            height.write_le(&mut writer)?;
382            hash.write_le(&mut writer)?;
383        }
384        Ok(())
385    }
386}
387
388#[cfg(any(test, feature = "test"))]
389pub mod test_helpers {
390    use super::*;
391    use snarkvm::prelude::Field;
392
393    type CurrentNetwork = snarkvm::prelude::MainnetV0;
394
395    /// Simulates a block locator at the given height.
396    ///
397    /// The returned block locator is checked to be well-formed.
398    pub fn sample_block_locators(height: u32) -> BlockLocators<CurrentNetwork> {
399        // Create the recent locators.
400        let mut recents = IndexMap::new();
401        let recents_range = match height < NUM_RECENT_BLOCKS as u32 {
402            true => 0..=height,
403            false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height,
404        };
405        for i in recents_range {
406            recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
407        }
408
409        // Create the checkpoint locators.
410        let mut checkpoints = IndexMap::new();
411        for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
412            checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
413        }
414
415        // Construct the block locators.
416        BlockLocators::new(recents, checkpoints).unwrap()
417    }
418
419    /// Simulates a block locator at the given height, with a fork within NUM_RECENT_BLOCKS of the given height.
420    ///
421    /// The returned block locator is checked to be well-formed.
422    pub fn sample_block_locators_with_fork(height: u32, fork_height: u32) -> BlockLocators<CurrentNetwork> {
423        assert!(fork_height <= height, "Fork height must be less than or equal to the given height");
424        assert!(
425            height - fork_height < NUM_RECENT_BLOCKS as u32,
426            "Fork must be within NUM_RECENT_BLOCKS of the given height"
427        );
428
429        // Create the recent locators.
430        let mut recents = IndexMap::new();
431        let recents_range = match height < NUM_RECENT_BLOCKS as u32 {
432            true => 0..=height,
433            false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height,
434        };
435        for i in recents_range {
436            if i >= fork_height {
437                recents.insert(i, (-Field::<CurrentNetwork>::from_u32(i)).into());
438            } else {
439                recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
440            }
441        }
442
443        // Create the checkpoint locators.
444        let mut checkpoints = IndexMap::new();
445        for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
446            checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
447        }
448
449        // Construct the block locators.
450        BlockLocators::new(recents, checkpoints).unwrap()
451    }
452
453    /// A test to ensure that the sample block locators are valid.
454    #[test]
455    fn test_sample_block_locators() {
456        for expected_height in 0..=100_001u32 {
457            println!("Testing height - {expected_height}");
458
459            let expected_num_checkpoints = (expected_height / CHECKPOINT_INTERVAL) + 1;
460            let expected_num_recents = match expected_height < NUM_RECENT_BLOCKS as u32 {
461                true => expected_height + 1,
462                false => NUM_RECENT_BLOCKS as u32,
463            };
464
465            let block_locators = sample_block_locators(expected_height);
466            assert_eq!(block_locators.checkpoints.len(), expected_num_checkpoints as usize);
467            assert_eq!(block_locators.recents.len(), expected_num_recents as usize);
468            assert_eq!(block_locators.latest_locator_height(), expected_height);
469            // Note that `sample_block_locators` always returns well-formed block locators,
470            // so we don't need to check `is_valid()` here.
471        }
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use snarkvm::prelude::Field;
479
480    use core::ops::Range;
481
482    type CurrentNetwork = snarkvm::prelude::MainnetV0;
483
484    /// Simulates block locators for a ledger within the given `heights` range.
485    fn check_is_valid(checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>, heights: Range<u32>) {
486        for height in heights {
487            let mut recents = IndexMap::new();
488            for i in 0..NUM_RECENT_BLOCKS as u32 {
489                recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
490
491                let block_locators =
492                    BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
493                if height == 0 && recents.len() < NUM_RECENT_BLOCKS {
494                    // For the first NUM_RECENT_BLOCKS, ensure NUM_RECENT_BLOCKS - 1 or less is valid.
495                    block_locators.ensure_is_valid().unwrap();
496                } else if recents.len() < NUM_RECENT_BLOCKS {
497                    // After the first NUM_RECENT_BLOCKS blocks from genesis, ensure NUM_RECENT_BLOCKS - 1 or less is not valid.
498                    block_locators.ensure_is_valid().unwrap_err();
499                } else {
500                    // After the first NUM_RECENT_BLOCKS blocks from genesis, ensure NUM_RECENT_BLOCKS is valid.
501                    block_locators.ensure_is_valid().unwrap();
502                }
503            }
504            // Ensure NUM_RECENT_BLOCKS + 1 is not valid.
505            recents.insert(
506                height + NUM_RECENT_BLOCKS as u32,
507                (Field::<CurrentNetwork>::from_u32(height + NUM_RECENT_BLOCKS as u32)).into(),
508            );
509            let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
510            block_locators.ensure_is_valid().unwrap_err();
511        }
512    }
513
514    /// Simulates block locators for a ledger within the given `heights` range.
515    fn check_is_consistent(
516        checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>,
517        heights: Range<u32>,
518        genesis_locators: BlockLocators<CurrentNetwork>,
519        second_locators: BlockLocators<CurrentNetwork>,
520    ) {
521        for height in heights {
522            let mut recents = IndexMap::new();
523            for i in 0..NUM_RECENT_BLOCKS as u32 {
524                recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
525
526                let block_locators =
527                    BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
528                block_locators.ensure_is_consistent_with(&block_locators).unwrap();
529
530                // Only test consistency when the block locators are valid to begin with.
531                let is_first_num_recents_blocks = height == 0 && recents.len() < NUM_RECENT_BLOCKS;
532                let is_num_recents_blocks = recents.len() == NUM_RECENT_BLOCKS;
533                if is_first_num_recents_blocks || is_num_recents_blocks {
534                    // Ensure the block locators are consistent with the genesis block locators.
535                    genesis_locators.ensure_is_consistent_with(&block_locators).unwrap();
536                    block_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
537
538                    // Ensure the block locators are consistent with the block locators with two recent blocks.
539                    second_locators.ensure_is_consistent_with(&block_locators).unwrap();
540                    block_locators.ensure_is_consistent_with(&second_locators).unwrap();
541                }
542            }
543        }
544    }
545
546    #[test]
547    fn test_ensure_is_valid() {
548        let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
549        let checkpoint_1: <CurrentNetwork as Network>::BlockHash =
550            (Field::<CurrentNetwork>::from_u32(CHECKPOINT_INTERVAL)).into();
551
552        // Ensure the block locators are valid.
553        for height in 0..10 {
554            let block_locators = test_helpers::sample_block_locators(height);
555            block_locators.ensure_is_valid().unwrap();
556        }
557
558        // Ensure the first NUM_RECENT blocks are valid.
559        let checkpoints = IndexMap::from([(0, zero)]);
560        let mut recents = IndexMap::new();
561        for i in 0..NUM_RECENT_BLOCKS {
562            recents.insert(i as u32, (Field::<CurrentNetwork>::from_u32(i as u32)).into());
563            let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
564            block_locators.ensure_is_valid().unwrap();
565        }
566        // Ensure NUM_RECENT_BLOCKS + 1 is not valid.
567        recents.insert(NUM_RECENT_BLOCKS as u32, (Field::<CurrentNetwork>::from_u32(NUM_RECENT_BLOCKS as u32)).into());
568        let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints);
569        block_locators.ensure_is_valid().unwrap_err();
570
571        // Ensure block locators before the second checkpoint are valid.
572        let checkpoints = IndexMap::from([(0, zero)]);
573        check_is_valid(checkpoints, 0..(CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32));
574
575        // Ensure the block locators after the second checkpoint are valid.
576        let checkpoints = IndexMap::from([(0, zero), (CHECKPOINT_INTERVAL, checkpoint_1)]);
577        check_is_valid(
578            checkpoints,
579            (CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32 + 1)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENT_BLOCKS as u32),
580        );
581    }
582
583    #[test]
584    fn test_ensure_is_valid_fails() {
585        let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
586        let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
587
588        // Ensure an empty block locators is not valid.
589        let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(Default::default(), Default::default());
590        block_locators.ensure_is_valid().unwrap_err();
591
592        // Ensure internally-mismatching genesis block locators is not valid.
593        let block_locators =
594            BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, zero)]), IndexMap::from([(0, one)]));
595        block_locators.ensure_is_valid().unwrap_err();
596
597        // Ensure internally-mismatching genesis block locators is not valid.
598        let block_locators =
599            BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, one)]), IndexMap::from([(0, zero)]));
600        block_locators.ensure_is_valid().unwrap_err();
601
602        // Ensure internally-mismatching block locators with two recent blocks is not valid.
603        let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
604            IndexMap::from([(0, one), (1, zero)]),
605            IndexMap::from([(0, zero)]),
606        );
607        block_locators.ensure_is_valid().unwrap_err();
608
609        // Ensure duplicate recent block hashes are not valid.
610        let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
611            IndexMap::from([(0, zero), (1, zero)]),
612            IndexMap::from([(0, zero)]),
613        );
614        block_locators.ensure_is_valid().unwrap_err();
615
616        // Ensure insufficient checkpoints are not valid.
617        let mut recents = IndexMap::new();
618        for i in 0..NUM_RECENT_BLOCKS {
619            recents.insert(10_000 + i as u32, (Field::<CurrentNetwork>::from_u32(i as u32)).into());
620        }
621        let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents, IndexMap::from([(0, zero)]));
622        block_locators.ensure_is_valid().unwrap_err();
623    }
624
625    #[test]
626    fn test_ensure_is_consistent_with() {
627        let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
628        let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
629
630        let genesis_locators =
631            BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)]));
632        let second_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
633            IndexMap::from([(0, zero), (1, one)]),
634            IndexMap::from([(0, zero)]),
635        );
636
637        // Ensure genesis block locators is consistent with genesis block locators.
638        genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
639
640        // Ensure genesis block locators is consistent with block locators with two recent blocks.
641        genesis_locators.ensure_is_consistent_with(&second_locators).unwrap();
642        second_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
643
644        // Ensure the block locators before the second checkpoint are valid.
645        let checkpoints = IndexMap::from([(0, Default::default())]);
646        check_is_consistent(
647            checkpoints,
648            0..(CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32),
649            genesis_locators.clone(),
650            second_locators.clone(),
651        );
652
653        // Ensure the block locators after the second checkpoint are valid.
654        let checkpoints = IndexMap::from([(0, Default::default()), (CHECKPOINT_INTERVAL, Default::default())]);
655        check_is_consistent(
656            checkpoints,
657            (CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENT_BLOCKS as u32),
658            genesis_locators,
659            second_locators,
660        );
661    }
662
663    #[test]
664    fn test_ensure_is_consistent_with_fails() {
665        let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
666        let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
667
668        let genesis_locators =
669            BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)])).unwrap();
670        let second_locators =
671            BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero), (1, one)]), IndexMap::from([(0, zero)]))
672                .unwrap();
673
674        let wrong_genesis_locators =
675            BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one)]), IndexMap::from([(0, one)])).unwrap();
676        let wrong_second_locators =
677            BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one), (1, zero)]), IndexMap::from([(0, one)]))
678                .unwrap();
679
680        genesis_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
681        wrong_genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
682
683        genesis_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
684        wrong_second_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
685
686        second_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
687        wrong_genesis_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
688
689        second_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
690        wrong_second_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
691    }
692}