amareleo_node_sync_locators/
block_locators.rs

1// Copyright 2024 Aleo Network Foundation
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
30#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
31pub struct BlockLocators<N: Network> {
32    /// The map of recent blocks.
33    pub recents: IndexMap<u32, N::BlockHash>,
34    /// The map of block checkpoints.
35    pub checkpoints: IndexMap<u32, N::BlockHash>,
36}
37
38impl<N: Network> BlockLocators<N> {
39    /// Initializes a new instance of the block locators, checking the validity of the block locators.
40    pub fn new(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Result<Self> {
41        // Construct the block locators.
42        let locators = Self { recents, checkpoints };
43        // Ensure the block locators are well-formed.
44        locators.ensure_is_valid()?;
45        // Return the block locators.
46        Ok(locators)
47    }
48
49    /// Initializes a new instance of the block locators, without checking the validity of the block locators.
50    pub fn new_unchecked(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Self {
51        Self { recents, checkpoints }
52    }
53
54    /// Initializes a new genesis instance of the block locators.
55    pub fn new_genesis(genesis_hash: N::BlockHash) -> Self {
56        Self { recents: indexmap![0 => genesis_hash], checkpoints: indexmap![0 => genesis_hash] }
57    }
58}
59
60impl<N: Network> IntoIterator for BlockLocators<N> {
61    type IntoIter = IntoIter<u32, N::BlockHash>;
62    type Item = (u32, N::BlockHash);
63
64    // TODO (howardwu): Consider using `BTreeMap::from_par_iter` if it is more performant.
65    //  Check by sorting 300-1000 items and comparing the performance.
66    //  (https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html#method.from_par_iter)
67    fn into_iter(self) -> Self::IntoIter {
68        BTreeMap::from_iter(self.checkpoints.into_iter().chain(self.recents)).into_iter()
69    }
70}
71
72impl<N: Network> BlockLocators<N> {
73    /// Returns the latest locator height.
74    pub fn latest_locator_height(&self) -> u32 {
75        self.recents.keys().last().copied().unwrap_or_default()
76    }
77
78    /// Returns the block hash for the given block height, if it exists.
79    pub fn get_hash(&self, height: u32) -> Option<N::BlockHash> {
80        self.recents.get(&height).copied().or_else(|| self.checkpoints.get(&height).copied())
81    }
82
83    /// Returns `true` if the block locators are well-formed.
84    pub fn is_valid(&self) -> bool {
85        // Ensure the block locators are well-formed.
86        if let Err(error) = self.ensure_is_valid() {
87            warn!("Block locators are invalid: {error}");
88            return false;
89        }
90        true
91    }
92
93    /// Returns `true` if the given block locators are consistent with this one.
94    /// This function assumes the given block locators are well-formed.
95    pub fn is_consistent_with(&self, other: &Self) -> bool {
96        // Ensure the block locators are consistent with the previous ones.
97        if let Err(error) = self.ensure_is_consistent_with(other) {
98            warn!("Inconsistent block locators: {error}");
99            return false;
100        }
101        true
102    }
103
104    /// Checks that this block locators are well-formed.
105    pub fn ensure_is_valid(&self) -> Result<()> {
106        // Ensure the block locators are well-formed.
107        Self::check_block_locators(&self.recents, &self.checkpoints)
108    }
109
110    /// Returns `true` if the given block locators are consistent with this one.
111    /// This function assumes the given block locators are well-formed.
112    pub fn ensure_is_consistent_with(&self, other: &Self) -> Result<()> {
113        Self::check_consistent_block_locators(self, other)
114    }
115}
116
117impl<N: Network> BlockLocators<N> {
118    /// Checks the old and new block locators share a consistent view of block history.
119    /// This function assumes the given block locators are well-formed.
120    pub fn check_consistent_block_locators(
121        old_locators: &BlockLocators<N>,
122        new_locators: &BlockLocators<N>,
123    ) -> Result<()> {
124        // For the overlapping recent blocks, ensure their block hashes match.
125        for (height, hash) in new_locators.recents.iter() {
126            if let Some(recent_hash) = old_locators.recents.get(height) {
127                if recent_hash != hash {
128                    bail!("Recent block hash mismatch at height {height}")
129                }
130            }
131        }
132        // For the overlapping block checkpoints, ensure their block hashes match.
133        for (height, hash) in new_locators.checkpoints.iter() {
134            if let Some(checkpoint_hash) = old_locators.checkpoints.get(height) {
135                if checkpoint_hash != hash {
136                    bail!("Block checkpoint hash mismatch for height {height}")
137                }
138            }
139        }
140        Ok(())
141    }
142
143    /// Checks that the block locators are well-formed.
144    pub fn check_block_locators(
145        recents: &IndexMap<u32, N::BlockHash>,
146        checkpoints: &IndexMap<u32, N::BlockHash>,
147    ) -> Result<()> {
148        // Ensure the recent blocks are well-formed.
149        let last_recent_height = Self::check_recent_blocks(recents)?;
150        // Ensure the block checkpoints are well-formed.
151        let last_checkpoint_height = Self::check_block_checkpoints(checkpoints)?;
152
153        // Ensure the `last_recent_height` is at or above `last_checkpoint_height - NUM_RECENT_BLOCKS`.
154        let threshold = last_checkpoint_height.saturating_sub(NUM_RECENT_BLOCKS as u32);
155        if last_recent_height < threshold {
156            bail!("Recent height ({last_recent_height}) cannot be below checkpoint threshold ({threshold})")
157        }
158
159        // If the `last_recent_height` is below NUM_RECENT_BLOCKS, ensure the genesis hash matches in both maps.
160        if last_recent_height < NUM_RECENT_BLOCKS as u32
161            && recents.get(&0).copied().unwrap_or_default() != checkpoints.get(&0).copied().unwrap_or_default()
162        {
163            bail!("Recent genesis hash and checkpoint genesis hash mismatch at height {last_recent_height}")
164        }
165
166        // If the `last_recent_height` overlaps with a checkpoint, ensure the block hashes match.
167        if let Some(last_checkpoint_hash) = checkpoints.get(&last_recent_height) {
168            if let Some(last_recent_hash) = recents.get(&last_recent_height) {
169                if last_checkpoint_hash != last_recent_hash {
170                    bail!("Recent block hash and checkpoint hash mismatch at height {last_recent_height}")
171                }
172            }
173        }
174        Ok(())
175    }
176
177    /// Checks the recent blocks, returning the last block height from the map.
178    ///
179    /// This function checks the following:
180    /// 1. The map is not empty.
181    /// 2. The map is at the correct interval.
182    /// 3. The map is at the correct height.
183    /// 4. The map is in the correct order.
184    /// 5. The map does not contain too many entries.
185    fn check_recent_blocks(recents: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
186        // Ensure the number of recent blocks is at least 1.
187        if recents.is_empty() {
188            bail!("There must be at least 1 recent block")
189        }
190        // Ensure the number of recent blocks is at most NUM_RECENT_BLOCKS.
191        // This redundant check ensures we early exit if the number of recent blocks is too large.
192        if recents.len() > NUM_RECENT_BLOCKS {
193            bail!("There can be at most {NUM_RECENT_BLOCKS} blocks in the map")
194        }
195
196        // Ensure the given recent blocks increment in height, and at the correct interval.
197        let mut last_height = 0;
198        for (i, current_height) in recents.keys().enumerate() {
199            if i == 0 && recents.len() < NUM_RECENT_BLOCKS && *current_height != last_height {
200                bail!("Ledgers under {NUM_RECENT_BLOCKS} blocks must have the first recent block at height 0")
201            }
202            if i > 0 && *current_height <= last_height {
203                bail!("Recent blocks must increment in height")
204            }
205            if i > 0 && *current_height - last_height != RECENT_INTERVAL {
206                bail!("Recent blocks must increment by {RECENT_INTERVAL}")
207            }
208            last_height = *current_height;
209        }
210
211        // If the last height is below NUM_RECENTS, ensure the number of recent blocks matches the last height.
212        if last_height < NUM_RECENT_BLOCKS as u32 && recents.len().saturating_sub(1) as u32 != last_height {
213            bail!("As the last height is below {NUM_RECENT_BLOCKS}, the number of recent blocks must match the height")
214        }
215        // Otherwise, ensure the number of recent blocks matches NUM_RECENT_BLOCKS.
216        if last_height >= NUM_RECENT_BLOCKS as u32 && recents.len() != NUM_RECENT_BLOCKS {
217            bail!("Number of recent blocks must match {NUM_RECENT_BLOCKS}")
218        }
219
220        // Ensure the block hashes are unique.
221        if has_duplicates(recents.values()) {
222            bail!("Recent block hashes must be unique")
223        }
224
225        Ok(last_height)
226    }
227
228    /// Checks the block checkpoints, returning the last block height from the checkpoints.
229    ///
230    /// This function checks the following:
231    /// 1. The block checkpoints are not empty.
232    /// 2. The block checkpoints are at the correct interval.
233    /// 3. The block checkpoints are at the correct height.
234    /// 4. The block checkpoints are in the correct order.
235    fn check_block_checkpoints(checkpoints: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
236        // Ensure the block checkpoints are not empty.
237        ensure!(!checkpoints.is_empty(), "There must be at least 1 block checkpoint");
238
239        // Ensure the given checkpoints increment in height, and at the correct interval.
240        let mut last_height = 0;
241        for (i, current_height) in checkpoints.keys().enumerate() {
242            if i == 0 && *current_height != 0 {
243                bail!("First block checkpoint must be at height 0")
244            }
245            if i > 0 && *current_height <= last_height {
246                bail!("Block checkpoints must increment in height")
247            }
248            if i > 0 && *current_height - last_height != CHECKPOINT_INTERVAL {
249                bail!("Block checkpoints must increment by {CHECKPOINT_INTERVAL}")
250            }
251            last_height = *current_height;
252        }
253
254        // Ensure the block hashes are unique.
255        if has_duplicates(checkpoints.values()) {
256            bail!("Block checkpoints must be unique")
257        }
258
259        Ok(last_height)
260    }
261}
262
263impl<N: Network> FromBytes for BlockLocators<N> {
264    fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
265        // Read the number of recent block hashes.
266        let num_recents = u32::read_le(&mut reader)?;
267        // Read the recent block hashes.
268        let mut recents = IndexMap::new();
269        for _ in 0..num_recents {
270            let height = u32::read_le(&mut reader)?;
271            let hash = N::BlockHash::read_le(&mut reader)?;
272            recents.insert(height, hash);
273        }
274
275        // Read the number of checkpoints.
276        let num_checkpoints = u32::read_le(&mut reader)?;
277        // Read the checkpoints.
278        let mut checkpoints = IndexMap::new();
279        for _ in 0..num_checkpoints {
280            let height = u32::read_le(&mut reader)?;
281            let hash = N::BlockHash::read_le(&mut reader)?;
282            checkpoints.insert(height, hash);
283        }
284
285        Self::new(recents, checkpoints).map_err(error)
286    }
287}
288
289impl<N: Network> ToBytes for BlockLocators<N> {
290    fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
291        // Write the number of recent block hashes.
292        u32::try_from(self.recents.len()).map_err(error)?.write_le(&mut writer)?;
293        // Write the recent block hashes.
294        for (height, hash) in &self.recents {
295            height.write_le(&mut writer)?;
296            hash.write_le(&mut writer)?;
297        }
298
299        // Write the number of checkpoints.
300        u32::try_from(self.checkpoints.len()).map_err(error)?.write_le(&mut writer)?;
301        // Write the checkpoints.
302        for (height, hash) in &self.checkpoints {
303            height.write_le(&mut writer)?;
304            hash.write_le(&mut writer)?;
305        }
306        Ok(())
307    }
308}
309
310#[cfg(any(test, feature = "test"))]
311pub mod test_helpers {
312    use super::*;
313    use snarkvm::prelude::Field;
314
315    type CurrentNetwork = snarkvm::prelude::MainnetV0;
316
317    /// Simulates a block locator at the given height.
318    pub fn sample_block_locators(height: u32) -> BlockLocators<CurrentNetwork> {
319        // Create the recent locators.
320        let mut recents = IndexMap::new();
321        let recents_range = match height < NUM_RECENT_BLOCKS as u32 {
322            true => 0..=height,
323            false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height,
324        };
325        for i in recents_range {
326            recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
327        }
328
329        // Create the checkpoint locators.
330        let mut checkpoints = IndexMap::new();
331        for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
332            checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
333        }
334
335        // Construct the block locators.
336        BlockLocators::new(recents, checkpoints).unwrap()
337    }
338
339    /// Simulates a block locator at the given height, with a fork within NUM_RECENTS of the given height.
340    pub fn sample_block_locators_with_fork(height: u32, fork_height: u32) -> BlockLocators<CurrentNetwork> {
341        assert!(fork_height <= height, "Fork height must be less than or equal to the given height");
342        assert!(height - fork_height < NUM_RECENT_BLOCKS as u32, "Fork must be within NUM_RECENTS of the given height");
343
344        // Create the recent locators.
345        let mut recents = IndexMap::new();
346        let recents_range = match height < NUM_RECENT_BLOCKS as u32 {
347            true => 0..=height,
348            false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height,
349        };
350        for i in recents_range {
351            if i >= fork_height {
352                recents.insert(i, (-Field::<CurrentNetwork>::from_u32(i)).into());
353            } else {
354                recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
355            }
356        }
357
358        // Create the checkpoint locators.
359        let mut checkpoints = IndexMap::new();
360        for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
361            checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
362        }
363
364        // Construct the block locators.
365        BlockLocators::new(recents, checkpoints).unwrap()
366    }
367
368    /// A test to ensure that the sample block locators are valid.
369    #[test]
370    fn test_sample_block_locators() {
371        for expected_height in 0..=100_001u32 {
372            println!("Testing height - {expected_height}");
373
374            let expected_num_checkpoints = (expected_height / CHECKPOINT_INTERVAL) + 1;
375            let expected_num_recents = match expected_height < NUM_RECENT_BLOCKS as u32 {
376                true => expected_height + 1,
377                false => NUM_RECENT_BLOCKS as u32,
378            };
379
380            let block_locators = sample_block_locators(expected_height);
381            assert_eq!(block_locators.checkpoints.len(), expected_num_checkpoints as usize);
382            assert_eq!(block_locators.recents.len(), expected_num_recents as usize);
383            assert_eq!(block_locators.latest_locator_height(), expected_height);
384            assert!(block_locators.is_valid());
385        }
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392    use snarkvm::prelude::Field;
393
394    use core::ops::Range;
395
396    type CurrentNetwork = snarkvm::prelude::MainnetV0;
397
398    /// Simulates block locators for a ledger within the given `heights` range.
399    fn check_is_valid(checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>, heights: Range<u32>) {
400        for height in heights {
401            let mut recents = IndexMap::new();
402            for i in 0..NUM_RECENT_BLOCKS as u32 {
403                recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
404
405                let block_locators =
406                    BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
407                if height == 0 && recents.len() < NUM_RECENT_BLOCKS {
408                    // For the first NUM_RECENTS blocks, ensure NUM_RECENTS - 1 or less is valid.
409                    block_locators.ensure_is_valid().unwrap();
410                } else if recents.len() < NUM_RECENT_BLOCKS {
411                    // After the first NUM_RECENTS blocks from genesis, ensure NUM_RECENTS - 1 or less is not valid.
412                    block_locators.ensure_is_valid().unwrap_err();
413                } else {
414                    // After the first NUM_RECENTS blocks from genesis, ensure NUM_RECENTS is valid.
415                    block_locators.ensure_is_valid().unwrap();
416                }
417            }
418            // Ensure NUM_RECENTS + 1 is not valid.
419            recents.insert(
420                height + NUM_RECENT_BLOCKS as u32,
421                (Field::<CurrentNetwork>::from_u32(height + NUM_RECENT_BLOCKS as u32)).into(),
422            );
423            let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
424            block_locators.ensure_is_valid().unwrap_err();
425        }
426    }
427
428    /// Simulates block locators for a ledger within the given `heights` range.
429    fn check_is_consistent(
430        checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>,
431        heights: Range<u32>,
432        genesis_locators: BlockLocators<CurrentNetwork>,
433        second_locators: BlockLocators<CurrentNetwork>,
434    ) {
435        for height in heights {
436            let mut recents = IndexMap::new();
437            for i in 0..NUM_RECENT_BLOCKS as u32 {
438                recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
439
440                let block_locators =
441                    BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
442                block_locators.ensure_is_consistent_with(&block_locators).unwrap();
443
444                // Only test consistency when the block locators are valid to begin with.
445                let is_first_num_recents_blocks = height == 0 && recents.len() < NUM_RECENT_BLOCKS;
446                let is_num_recents_blocks = recents.len() == NUM_RECENT_BLOCKS;
447                if is_first_num_recents_blocks || is_num_recents_blocks {
448                    // Ensure the block locators are consistent with the genesis block locators.
449                    genesis_locators.ensure_is_consistent_with(&block_locators).unwrap();
450                    block_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
451
452                    // Ensure the block locators are consistent with the block locators with two recent blocks.
453                    second_locators.ensure_is_consistent_with(&block_locators).unwrap();
454                    block_locators.ensure_is_consistent_with(&second_locators).unwrap();
455                }
456            }
457        }
458    }
459
460    #[test]
461    fn test_ensure_is_valid() {
462        let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
463        let checkpoint_1: <CurrentNetwork as Network>::BlockHash =
464            (Field::<CurrentNetwork>::from_u32(CHECKPOINT_INTERVAL)).into();
465
466        // Ensure the block locators are valid.
467        for height in 0..10 {
468            let block_locators = test_helpers::sample_block_locators(height);
469            block_locators.ensure_is_valid().unwrap();
470        }
471
472        // Ensure the first NUM_RECENT blocks are valid.
473        let checkpoints = IndexMap::from([(0, zero)]);
474        let mut recents = IndexMap::new();
475        for i in 0..NUM_RECENT_BLOCKS {
476            recents.insert(i as u32, (Field::<CurrentNetwork>::from_u32(i as u32)).into());
477            let block_locators = BlockLocators::<CurrentNetwork>::new(recents.clone(), checkpoints.clone()).unwrap();
478            block_locators.ensure_is_valid().unwrap();
479        }
480        // Ensure NUM_RECENTS + 1 is not valid.
481        recents.insert(NUM_RECENT_BLOCKS as u32, (Field::<CurrentNetwork>::from_u32(NUM_RECENT_BLOCKS as u32)).into());
482        let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints);
483        block_locators.ensure_is_valid().unwrap_err();
484
485        // Ensure block locators before the second checkpoint are valid.
486        let checkpoints = IndexMap::from([(0, zero)]);
487        check_is_valid(checkpoints, 0..(CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32));
488
489        // Ensure the block locators after the second checkpoint are valid.
490        let checkpoints = IndexMap::from([(0, zero), (CHECKPOINT_INTERVAL, checkpoint_1)]);
491        check_is_valid(
492            checkpoints,
493            (CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENT_BLOCKS as u32),
494        );
495    }
496
497    #[test]
498    fn test_ensure_is_valid_fails() {
499        let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
500        let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
501
502        // Ensure an empty block locators is not valid.
503        let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(Default::default(), Default::default());
504        block_locators.ensure_is_valid().unwrap_err();
505
506        // Ensure internally-mismatching genesis block locators is valid.
507        let block_locators =
508            BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, zero)]), IndexMap::from([(0, one)]));
509        block_locators.ensure_is_valid().unwrap_err();
510
511        // Ensure internally-mismatching genesis block locators is valid.
512        let block_locators =
513            BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, one)]), IndexMap::from([(0, zero)]));
514        block_locators.ensure_is_valid().unwrap_err();
515
516        // Ensure internally-mismatching block locators with two recent blocks is valid.
517        let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
518            IndexMap::from([(0, one), (1, zero)]),
519            IndexMap::from([(0, zero)]),
520        );
521        block_locators.ensure_is_valid().unwrap_err();
522
523        // Ensure duplicate recent block hashes are not valid.
524        let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
525            IndexMap::from([(0, zero), (1, zero)]),
526            IndexMap::from([(0, zero)]),
527        );
528        block_locators.ensure_is_valid().unwrap_err();
529    }
530
531    #[test]
532    fn test_ensure_is_consistent_with() {
533        let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
534        let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
535
536        let genesis_locators =
537            BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)]));
538        let second_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
539            IndexMap::from([(0, zero), (1, one)]),
540            IndexMap::from([(0, zero)]),
541        );
542
543        // Ensure genesis block locators is consistent with genesis block locators.
544        genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
545
546        // Ensure genesis block locators is consistent with block locators with two recent blocks.
547        genesis_locators.ensure_is_consistent_with(&second_locators).unwrap();
548        second_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
549
550        // Ensure the block locators before the second checkpoint are valid.
551        let checkpoints = IndexMap::from([(0, Default::default())]);
552        check_is_consistent(
553            checkpoints,
554            0..(CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32),
555            genesis_locators.clone(),
556            second_locators.clone(),
557        );
558
559        // Ensure the block locators after the second checkpoint are valid.
560        let checkpoints = IndexMap::from([(0, Default::default()), (CHECKPOINT_INTERVAL, Default::default())]);
561        check_is_consistent(
562            checkpoints,
563            (CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENT_BLOCKS as u32),
564            genesis_locators,
565            second_locators,
566        );
567    }
568
569    #[test]
570    fn test_ensure_is_consistent_with_fails() {
571        let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
572        let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
573
574        let genesis_locators =
575            BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)])).unwrap();
576        let second_locators =
577            BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero), (1, one)]), IndexMap::from([(0, zero)]))
578                .unwrap();
579
580        let wrong_genesis_locators =
581            BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one)]), IndexMap::from([(0, one)])).unwrap();
582        let wrong_second_locators =
583            BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one), (1, zero)]), IndexMap::from([(0, one)]))
584                .unwrap();
585
586        genesis_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
587        wrong_genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
588
589        genesis_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
590        wrong_second_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
591
592        second_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
593        wrong_genesis_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
594
595        second_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
596        wrong_second_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
597    }
598}