Skip to main content

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