cuprate_pruning/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
//! # Pruning Mechanism for Monero
//!
//! This crate provides an implementation of the pruning mechanism used in Monero.
//! The main data structure, `PruningSeed`, encapsulates the logic for creating and manipulating pruning seeds,
//! which determine the set of blocks to be pruned from the blockchain.
//!
//! `PruningSeed` also contains a method for checking if a pruning seed is valid for Monero rules (must only be
//! split into 8 parts):
//!
//! ```rust
//! use cuprate_pruning::PruningSeed;
//!
//! let seed: u32 = 386; // the seed you want to check is valid
//! match PruningSeed::decompress_p2p_rules(seed) {
//!     Ok(seed) => seed, // seed is valid
//!     Err(e) => panic!("seed is invalid")
//! };
//! ```
//!

use std::cmp::Ordering;

use cuprate_constants::block::MAX_BLOCK_HEIGHT_USIZE;

use thiserror::Error;

/// The default log stripes for Monero pruning.
pub const CRYPTONOTE_PRUNING_LOG_STRIPES: u32 = 3;
/// The amount of blocks that peers keep before another stripe starts storing blocks.
pub const CRYPTONOTE_PRUNING_STRIPE_SIZE: usize = 4096;
/// The amount of blocks from the top of the chain that should not be pruned.
pub const CRYPTONOTE_PRUNING_TIP_BLOCKS: usize = 5500;

const PRUNING_SEED_LOG_STRIPES_SHIFT: u32 = 7;
const PRUNING_SEED_STRIPE_SHIFT: u32 = 0;
const PRUNING_SEED_LOG_STRIPES_MASK: u32 = 0x7;
const PRUNING_SEED_STRIPE_MASK: u32 = 127;

#[derive(Debug, Error)]
pub enum PruningError {
    #[error("log_stripes is out of range")]
    LogStripesOutOfRange,
    #[error("Stripe is out of range")]
    StripeOutOfRange,
    #[error("The block height is greater than `MAX_BLOCK_HEIGHT_USIZE`")]
    BlockHeightTooLarge,
    #[error("The blockchain height is greater than `MAX_BLOCK_HEIGHT_USIZE`")]
    BlockChainHeightTooLarge,
    #[error("The calculated height is smaller than the block height entered")]
    CalculatedHeightSmallerThanEnteredBlock,
    #[error("The entered seed has incorrect log stripes")]
    SeedDoesNotHaveCorrectLogStripes,
}

/// A valid pruning seed for a Monero node.
///
/// A pruning seed tells nodes which blocks they should keep and which they should prune.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[cfg_attr(
    feature = "borsh",
    derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
pub enum PruningSeed {
    /// A peer with this seed is not pruned.
    NotPruned,
    /// A peer with this seed is pruned.
    Pruned(DecompressedPruningSeed),
}

impl PruningSeed {
    /// Creates a new [`PruningSeed::Pruned`] seed.
    ///
    /// See: [`DecompressedPruningSeed::new`]
    pub fn new_pruned(stripe: u32, log_stripes: u32) -> Result<Self, PruningError> {
        Ok(Self::Pruned(DecompressedPruningSeed::new(
            stripe,
            log_stripes,
        )?))
    }

    /// Attempts to decompress a raw pruning seed.
    ///
    /// An error means the pruning seed was invalid.
    pub fn decompress(seed: u32) -> Result<Self, PruningError> {
        Ok(DecompressedPruningSeed::decompress(seed)?.map_or(Self::NotPruned, Self::Pruned))
    }

    /// Decompresses the seed, performing the same checks as [`PruningSeed::decompress`] and some more according to
    /// Monero's p2p networks rules.
    ///
    /// The only added check currently is that `log_stripes` == 3.
    pub fn decompress_p2p_rules(seed: u32) -> Result<Self, PruningError> {
        let seed = Self::decompress(seed)?;

        if let Some(log_stripes) = seed.get_log_stripes() {
            if log_stripes != CRYPTONOTE_PRUNING_LOG_STRIPES {
                return Err(PruningError::LogStripesOutOfRange);
            }
        }

        Ok(seed)
    }

    /// Compresses this pruning seed to a u32.
    pub const fn compress(&self) -> u32 {
        match self {
            Self::NotPruned => 0,
            Self::Pruned(seed) => seed.compress(),
        }
    }

    /// Returns the `log_stripes` for this seed, if this seed is pruned otherwise [`None`] is returned.
    pub const fn get_log_stripes(&self) -> Option<u32> {
        match self {
            Self::NotPruned => None,
            Self::Pruned(seed) => Some(seed.log_stripes),
        }
    }

    /// Returns the `stripe` for this seed, if this seed is pruned otherwise [`None`] is returned.
    pub const fn get_stripe(&self) -> Option<u32> {
        match self {
            Self::NotPruned => None,
            Self::Pruned(seed) => Some(seed.stripe),
        }
    }

    /// Returns `true` if a peer with this pruning seed should have a non-pruned version of a block.
    pub const fn has_full_block(&self, height: usize, blockchain_height: usize) -> bool {
        match self {
            Self::NotPruned => true,
            Self::Pruned(seed) => seed.has_full_block(height, blockchain_height),
        }
    }

    /// Gets the next pruned block for a given `block_height` and `blockchain_height`
    ///
    /// Each seed will store, in a cyclic manner, a portion of blocks while discarding
    /// the ones that are out of your stripe. This function is finding the next height
    /// for which a specific seed will start pruning blocks.
    ///
    /// This will return Ok(None) if the seed does no pruning or if there is no pruned block
    /// after this one.
    ///
    /// ### Errors
    ///
    /// This function will return an Error if the inputted `block_height` or
    /// `blockchain_height` is greater than [`MAX_BLOCK_HEIGHT_USIZE`].
    ///
    /// This function will also error if `block_height` > `blockchain_height`
    pub fn get_next_pruned_block(
        &self,
        block_height: usize,
        blockchain_height: usize,
    ) -> Result<Option<usize>, PruningError> {
        Ok(match self {
            Self::NotPruned => None,
            Self::Pruned(seed) => seed.get_next_pruned_block(block_height, blockchain_height)?,
        })
    }

    /// Gets the next unpruned block for a given `block_height` and `blockchain_height`
    ///
    /// Each seed will store, in a cyclic manner, a portion of blocks while discarding
    /// the ones that are out of your stripe. This function is finding the next height
    /// for which a specific seed will start storing blocks.
    ///
    /// ### Errors
    ///
    /// This function will return an Error if the inputted `block_height` or
    /// `blockchain_height` is greater than [`MAX_BLOCK_HEIGHT_USIZE`].
    ///
    /// This function will also error if `block_height` > `blockchain_height`
    ///
    pub fn get_next_unpruned_block(
        &self,
        block_height: usize,
        blockchain_height: usize,
    ) -> Result<usize, PruningError> {
        Ok(match self {
            Self::NotPruned => block_height,
            Self::Pruned(seed) => seed.get_next_unpruned_block(block_height, blockchain_height)?,
        })
    }
}

impl PartialOrd<Self> for PruningSeed {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for PruningSeed {
    fn cmp(&self, other: &Self) -> Ordering {
        match (self, other) {
            // Make sure pruning seeds storing more blocks are greater.
            (Self::NotPruned, Self::NotPruned) => Ordering::Equal,
            (Self::NotPruned, Self::Pruned(_)) => Ordering::Greater,
            (Self::Pruned(_), Self::NotPruned) => Ordering::Less,

            (Self::Pruned(seed1), Self::Pruned(seed2)) => seed1.cmp(seed2),
        }
    }
}

/// This represents a valid Monero pruning seed.
///
/// It does allow representations of pruning seeds that Monero's P2P network would not allow, i.e.
/// it does not restrict the seed to only have a `log_stripes` of 8.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[cfg_attr(
    feature = "borsh",
    derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
pub struct DecompressedPruningSeed {
    /// The amount of portions the blockchain is split into.
    log_stripes: u32,
    /// The specific portion this peer keeps.
    ///
    /// *MUST* be between `1..=2^log_stripes`
    stripe: u32,
}

impl PartialOrd<Self> for DecompressedPruningSeed {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for DecompressedPruningSeed {
    fn cmp(&self, other: &Self) -> Ordering {
        // Compare the `log_stripes` first so peers which store more blocks are greater than peers
        // storing less.
        match self.log_stripes.cmp(&other.log_stripes) {
            Ordering::Equal => self.stripe.cmp(&other.stripe),
            ord => ord,
        }
    }
}

impl DecompressedPruningSeed {
    /// Creates a new pruning seed from a `stripe` and `log_stripes`
    ///
    /// ### What is a `stripe`
    ///
    /// A stripe is the part of the blockchain this peer will keep.
    ///  
    /// Monero, when pruning, will split the blockchain into multiple
    /// "stripes", that amount is currently 8 and each pruned peer
    /// will keep one of those 8 stripes.
    ///
    /// ### What is `log_stripes`
    ///
    /// `log_stripes` is log2 of the amount of stripes used.
    ///
    ///  For Monero, currently, that amount is 8 so `log_stripes` will
    ///  be 3.
    ///
    /// ---------------------------------------------------------------
    ///
    /// *note this function allows you to make invalid seeds, this is done
    /// to allow the specifics of pruning to change in the future. To make
    /// a valid seed you currently MUST pass in a number 1 to 8 for `stripe`
    /// and 3 for `log_stripes`.*
    ///
    pub const fn new(stripe: u32, log_stripes: u32) -> Result<Self, PruningError> {
        if log_stripes > PRUNING_SEED_LOG_STRIPES_MASK {
            Err(PruningError::LogStripesOutOfRange)
        } else if !(stripe > 0 && stripe <= (1 << log_stripes)) {
            Err(PruningError::StripeOutOfRange)
        } else {
            Ok(Self {
                log_stripes,
                stripe,
            })
        }
    }

    /// Attempts to decompress a raw pruning seed.
    ///
    /// Will return Ok(None) if the pruning seed means no pruning.
    ///
    /// An error means the pruning seed was invalid.
    pub const fn decompress(seed: u32) -> Result<Option<Self>, PruningError> {
        if seed == 0 {
            // No pruning.
            return Ok(None);
        }

        let log_stripes = (seed >> PRUNING_SEED_LOG_STRIPES_SHIFT) & PRUNING_SEED_LOG_STRIPES_MASK;
        let stripe = 1 + ((seed >> PRUNING_SEED_STRIPE_SHIFT) & PRUNING_SEED_STRIPE_MASK);

        if stripe > (1 << log_stripes) {
            return Err(PruningError::StripeOutOfRange);
        }

        Ok(Some(Self {
            log_stripes,
            stripe,
        }))
    }

    /// Compresses the pruning seed into a u32.
    pub const fn compress(&self) -> u32 {
        (self.log_stripes << PRUNING_SEED_LOG_STRIPES_SHIFT)
            | ((self.stripe - 1) << PRUNING_SEED_STRIPE_SHIFT)
    }

    /// Returns `true` if a peer with this pruning seed should have a non-pruned version of a block.
    pub const fn has_full_block(&self, height: usize, blockchain_height: usize) -> bool {
        match get_block_pruning_stripe(height, blockchain_height, self.log_stripes) {
            Some(block_stripe) => self.stripe == block_stripe,
            None => true,
        }
    }

    /// Gets the next unpruned block for a given `block_height` and `blockchain_height`
    ///
    /// Each seed will store, in a cyclic manner, a portion of blocks while discarding
    /// the ones that are out of your stripe. This function is finding the next height
    /// for which a specific seed will start storing blocks.
    ///
    /// ### Errors
    ///
    /// This function will return an Error if the inputted `block_height` or
    /// `blockchain_height` is greater than [`MAX_BLOCK_HEIGHT_USIZE`].
    ///
    /// This function will also error if `block_height` > `blockchain_height`
    ///
    pub fn get_next_unpruned_block(
        &self,
        block_height: usize,
        blockchain_height: usize,
    ) -> Result<usize, PruningError> {
        if block_height > MAX_BLOCK_HEIGHT_USIZE || block_height > blockchain_height {
            return Err(PruningError::BlockHeightTooLarge);
        }

        if blockchain_height > MAX_BLOCK_HEIGHT_USIZE {
            return Err(PruningError::BlockChainHeightTooLarge);
        }

        if block_height + CRYPTONOTE_PRUNING_TIP_BLOCKS >= blockchain_height {
            // If we are within `CRYPTONOTE_PRUNING_TIP_BLOCKS` of the chain we should
            // not prune blocks.
            return Ok(block_height);
        }

        let block_pruning_stripe = get_block_pruning_stripe(block_height, blockchain_height, self.log_stripes)
                .expect("We just checked if `block_height + CRYPTONOTE_PRUNING_TIP_BLOCKS >= blockchain_height`");
        if self.stripe == block_pruning_stripe {
            // if we have the same stripe as a block that means we keep the block so
            // the entered block is the next un-pruned one.
            return Ok(block_height);
        }

        // cycles: how many times each seed has stored blocks so when all seeds have
        // stored blocks thats 1 cycle
        let cycles = (block_height / CRYPTONOTE_PRUNING_STRIPE_SIZE) >> self.log_stripes;
        // if our seed is before the blocks seed in a cycle that means we have already past our
        // seed this cycle and need to start the next
        let cycles_start = cycles
            + if self.stripe > block_pruning_stripe {
                0
            } else {
                1
            };

        // amt_of_cycles * blocks in a cycle + how many blocks through a cycles until the seed starts storing blocks
        let calculated_height = cycles_start * (CRYPTONOTE_PRUNING_STRIPE_SIZE << self.log_stripes)
            + (self.stripe as usize - 1) * CRYPTONOTE_PRUNING_STRIPE_SIZE;

        if calculated_height + CRYPTONOTE_PRUNING_TIP_BLOCKS > blockchain_height {
            // if our calculated height is greater than the amount of tip blocks then the start of the tip blocks will be the next un-pruned
            Ok(blockchain_height.saturating_sub(CRYPTONOTE_PRUNING_TIP_BLOCKS))
        } else if calculated_height < block_height {
            Err(PruningError::CalculatedHeightSmallerThanEnteredBlock)
        } else {
            Ok(calculated_height)
        }
    }

    /// Gets the next pruned block for a given `block_height` and `blockchain_height`
    ///
    /// Each seed will store, in a cyclic manner, a portion of blocks while discarding
    /// the ones that are out of your stripe. This function is finding the next height
    /// for which a specific seed will start pruning blocks.
    ///
    /// ### Errors
    ///
    /// This function will return an Error if the inputted `block_height` or
    /// `blockchain_height` is greater than [`MAX_BLOCK_HEIGHT_USIZE`].
    ///
    /// This function will also error if `block_height` > `blockchain_height`
    ///
    pub fn get_next_pruned_block(
        &self,
        block_height: usize,
        blockchain_height: usize,
    ) -> Result<Option<usize>, PruningError> {
        if block_height + CRYPTONOTE_PRUNING_TIP_BLOCKS >= blockchain_height {
            // If we are within `CRYPTONOTE_PRUNING_TIP_BLOCKS` of the chain we should
            // not prune blocks.
            return Ok(None);
        }

        let block_pruning_stripe = get_block_pruning_stripe(block_height, blockchain_height, self.log_stripes)
            .expect("We just checked if `block_height + CRYPTONOTE_PRUNING_TIP_BLOCKS >= blockchain_height`");
        if self.stripe != block_pruning_stripe {
            // if our stripe != the blocks stripe that means we prune that block
            return Ok(Some(block_height));
        }

        // We can get the end of our "non-pruning" cycle by getting the next stripe's first un-pruned block height.
        // So we calculate the next un-pruned block for the next stripe and return it as our next pruned block
        let next_stripe = 1 + (self.stripe & ((1 << self.log_stripes) - 1));
        let seed = Self::new(next_stripe, self.log_stripes)
            .expect("We just made sure this stripe is in range for this log_stripe");

        let calculated_height = seed.get_next_unpruned_block(block_height, blockchain_height)?;

        if calculated_height + CRYPTONOTE_PRUNING_TIP_BLOCKS > blockchain_height {
            // If the calculated height is in tip blocks then there is no next block to prune
            Ok(None)
        } else {
            Ok(Some(calculated_height))
        }
    }
}

const fn get_block_pruning_stripe(
    block_height: usize,
    blockchain_height: usize,
    log_stripe: u32,
) -> Option<u32> {
    if block_height + CRYPTONOTE_PRUNING_TIP_BLOCKS >= blockchain_height {
        None
    } else {
        #[expect(
            clippy::cast_possible_truncation,
            clippy::cast_sign_loss,
            reason = "it's trivial to prove it's ok to us `as` here"
        )]
        Some(
            (((block_height / CRYPTONOTE_PRUNING_STRIPE_SIZE) & ((1 << log_stripe) as usize - 1))
                + 1) as u32,
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_all_pruning_seeds() -> Vec<PruningSeed> {
        let possible_stripes = 1..=(1 << CRYPTONOTE_PRUNING_LOG_STRIPES);
        possible_stripes
            .map(|stripe| PruningSeed::new_pruned(stripe, CRYPTONOTE_PRUNING_LOG_STRIPES).unwrap())
            .collect()
    }

    #[test]
    fn from_u32_for_pruning_seed() {
        let good_seeds = 384..=391;
        for seed in good_seeds {
            assert!(PruningSeed::decompress(seed).is_ok());
        }
        let bad_seeds = [383, 392];
        for seed in bad_seeds {
            assert!(PruningSeed::decompress(seed).is_err());
        }
    }

    #[test]
    fn make_invalid_pruning_seeds() {
        let invalid_stripes = [0, (1 << CRYPTONOTE_PRUNING_LOG_STRIPES) + 1];

        for stripe in invalid_stripes {
            assert!(PruningSeed::new_pruned(stripe, CRYPTONOTE_PRUNING_LOG_STRIPES).is_err());
        }
    }

    #[test]
    fn get_pruning_log_stripe() {
        let all_valid_seeds = make_all_pruning_seeds();
        for seed in &all_valid_seeds {
            assert_eq!(seed.get_log_stripes().unwrap(), 3);
        }
    }

    #[test]
    fn get_pruning_stripe() {
        let all_valid_seeds = make_all_pruning_seeds();
        #[expect(clippy::cast_possible_truncation)]
        for (i, seed) in all_valid_seeds.iter().enumerate() {
            assert_eq!(seed.get_stripe().unwrap(), i as u32 + 1);
        }
    }

    #[test]
    fn blocks_pruning_stripe() {
        let blockchain_height = 76437863;

        for i in 0_u32..8 {
            assert_eq!(
                get_block_pruning_stripe(
                    (i * 4096) as usize,
                    blockchain_height,
                    CRYPTONOTE_PRUNING_LOG_STRIPES
                )
                .unwrap(),
                i + 1
            );
        }

        for i in 0_u32..8 {
            assert_eq!(
                get_block_pruning_stripe(
                    32768 + (i * 4096) as usize,
                    blockchain_height,
                    CRYPTONOTE_PRUNING_LOG_STRIPES
                )
                .unwrap(),
                i + 1
            );
        }

        for i in 1_u32..8 {
            assert_eq!(
                get_block_pruning_stripe(
                    32767 + (i * 4096) as usize,
                    blockchain_height,
                    CRYPTONOTE_PRUNING_LOG_STRIPES
                )
                .unwrap(),
                i
            );
        }

        // Block shouldn't be pruned
        assert!(get_block_pruning_stripe(
            blockchain_height - 5500,
            blockchain_height,
            CRYPTONOTE_PRUNING_LOG_STRIPES
        )
        .is_none());
    }

    #[test]
    fn next_unpruned_block() {
        let all_valid_seeds = make_all_pruning_seeds();
        let blockchain_height = 76437863;

        for (i, seed) in all_valid_seeds.iter().enumerate() {
            assert_eq!(
                seed.get_next_unpruned_block(0, blockchain_height).unwrap(),
                i * 4096
            );
        }

        for (i, seed) in all_valid_seeds.iter().enumerate() {
            assert_eq!(
                seed.get_next_unpruned_block((i + 1) * 4096, blockchain_height)
                    .unwrap(),
                i * 4096 + 32768
            );
        }

        for (i, seed) in all_valid_seeds.iter().enumerate() {
            assert_eq!(
                seed.get_next_unpruned_block((i + 8) * 4096, blockchain_height)
                    .unwrap(),
                i * 4096 + 32768
            );
        }

        for seed in &all_valid_seeds {
            assert_eq!(
                seed.get_next_unpruned_block(76437863 - 1, blockchain_height)
                    .unwrap(),
                76437863 - 1
            );
        }

        let zero_seed = PruningSeed::NotPruned;

        assert_eq!(
            zero_seed.get_next_unpruned_block(33443, 5565445).unwrap(),
            33443
        );

        let seed = PruningSeed::decompress(384).unwrap();

        // the next unpruned block is the first tip block
        assert_eq!(seed.get_next_unpruned_block(5000, 11000).unwrap(), 5500);
    }

    #[test]
    fn next_pruned_block() {
        let all_valid_seeds = make_all_pruning_seeds();
        let blockchain_height = 76437863;

        for seed in all_valid_seeds.iter().skip(1) {
            assert_eq!(
                seed.get_next_pruned_block(0, blockchain_height)
                    .unwrap()
                    .unwrap(),
                0
            );
        }

        for (i, seed) in all_valid_seeds.iter().enumerate() {
            assert_eq!(
                seed.get_next_pruned_block((i + 1) * 4096, blockchain_height)
                    .unwrap()
                    .unwrap(),
                (i + 1) * 4096
            );
        }

        for (i, seed) in all_valid_seeds.iter().enumerate() {
            assert_eq!(
                seed.get_next_pruned_block((i + 8) * 4096, blockchain_height)
                    .unwrap()
                    .unwrap(),
                (i + 9) * 4096
            );
        }

        for seed in &all_valid_seeds {
            assert_eq!(
                seed.get_next_pruned_block(76437863 - 1, blockchain_height)
                    .unwrap(),
                None
            );
        }

        let zero_seed = PruningSeed::NotPruned;

        assert_eq!(
            zero_seed.get_next_pruned_block(33443, 5565445).unwrap(),
            None
        );

        let seed = PruningSeed::decompress(384).unwrap();

        // there is no next pruned block
        assert_eq!(seed.get_next_pruned_block(5000, 10000).unwrap(), None);
    }
}