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