1use std::cmp::Ordering;
22
23use cuprate_constants::block::MAX_BLOCK_HEIGHT_USIZE;
24
25use thiserror::Error;
26
27pub const CRYPTONOTE_PRUNING_LOG_STRIPES: u32 = 3;
29pub const CRYPTONOTE_PRUNING_STRIPE_SIZE: usize = 4096;
31pub const CRYPTONOTE_PRUNING_TIP_BLOCKS: usize = 5500;
33
34const PRUNING_SEED_LOG_STRIPES_SHIFT: u32 = 7;
35const PRUNING_SEED_STRIPE_SHIFT: u32 = 0;
36const PRUNING_SEED_LOG_STRIPES_MASK: u32 = 0x7;
37const PRUNING_SEED_STRIPE_MASK: u32 = 127;
38
39#[derive(Debug, Error)]
40pub enum PruningError {
41 #[error("log_stripes is out of range")]
42 LogStripesOutOfRange,
43 #[error("Stripe is out of range")]
44 StripeOutOfRange,
45 #[error("The block height is greater than `MAX_BLOCK_HEIGHT_USIZE`")]
46 BlockHeightTooLarge,
47 #[error("The blockchain height is greater than `MAX_BLOCK_HEIGHT_USIZE`")]
48 BlockChainHeightTooLarge,
49 #[error("The calculated height is smaller than the block height entered")]
50 CalculatedHeightSmallerThanEnteredBlock,
51 #[error("The entered seed has incorrect log stripes")]
52 SeedDoesNotHaveCorrectLogStripes,
53}
54
55#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
59#[cfg_attr(
60 feature = "borsh",
61 derive(borsh::BorshSerialize, borsh::BorshDeserialize)
62)]
63pub enum PruningSeed {
64 NotPruned,
66 Pruned(DecompressedPruningSeed),
68}
69
70impl PruningSeed {
71 pub fn new_pruned(stripe: u32, log_stripes: u32) -> Result<Self, PruningError> {
75 Ok(Self::Pruned(DecompressedPruningSeed::new(
76 stripe,
77 log_stripes,
78 )?))
79 }
80
81 pub fn decompress(seed: u32) -> Result<Self, PruningError> {
85 Ok(DecompressedPruningSeed::decompress(seed)?.map_or(Self::NotPruned, Self::Pruned))
86 }
87
88 pub fn decompress_p2p_rules(seed: u32) -> Result<Self, PruningError> {
93 let seed = Self::decompress(seed)?;
94
95 if let Some(log_stripes) = seed.get_log_stripes() {
96 if log_stripes != CRYPTONOTE_PRUNING_LOG_STRIPES {
97 return Err(PruningError::LogStripesOutOfRange);
98 }
99 }
100
101 Ok(seed)
102 }
103
104 pub const fn compress(&self) -> u32 {
106 match self {
107 Self::NotPruned => 0,
108 Self::Pruned(seed) => seed.compress(),
109 }
110 }
111
112 pub const fn get_log_stripes(&self) -> Option<u32> {
114 match self {
115 Self::NotPruned => None,
116 Self::Pruned(seed) => Some(seed.log_stripes),
117 }
118 }
119
120 pub const fn get_stripe(&self) -> Option<u32> {
122 match self {
123 Self::NotPruned => None,
124 Self::Pruned(seed) => Some(seed.stripe),
125 }
126 }
127
128 pub const fn has_full_block(&self, height: usize, blockchain_height: usize) -> bool {
130 match self {
131 Self::NotPruned => true,
132 Self::Pruned(seed) => seed.has_full_block(height, blockchain_height),
133 }
134 }
135
136 pub fn get_next_pruned_block(
152 &self,
153 block_height: usize,
154 blockchain_height: usize,
155 ) -> Result<Option<usize>, PruningError> {
156 Ok(match self {
157 Self::NotPruned => None,
158 Self::Pruned(seed) => seed.get_next_pruned_block(block_height, blockchain_height)?,
159 })
160 }
161
162 pub fn get_next_unpruned_block(
176 &self,
177 block_height: usize,
178 blockchain_height: usize,
179 ) -> Result<usize, PruningError> {
180 Ok(match self {
181 Self::NotPruned => block_height,
182 Self::Pruned(seed) => seed.get_next_unpruned_block(block_height, blockchain_height)?,
183 })
184 }
185}
186
187impl PartialOrd<Self> for PruningSeed {
188 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
189 Some(self.cmp(other))
190 }
191}
192
193impl Ord for PruningSeed {
194 fn cmp(&self, other: &Self) -> Ordering {
195 match (self, other) {
196 (Self::NotPruned, Self::NotPruned) => Ordering::Equal,
198 (Self::NotPruned, Self::Pruned(_)) => Ordering::Greater,
199 (Self::Pruned(_), Self::NotPruned) => Ordering::Less,
200
201 (Self::Pruned(seed1), Self::Pruned(seed2)) => seed1.cmp(seed2),
202 }
203 }
204}
205
206#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
211#[cfg_attr(
212 feature = "borsh",
213 derive(borsh::BorshSerialize, borsh::BorshDeserialize)
214)]
215pub struct DecompressedPruningSeed {
216 log_stripes: u32,
218 stripe: u32,
222}
223
224impl PartialOrd<Self> for DecompressedPruningSeed {
225 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
226 Some(self.cmp(other))
227 }
228}
229
230impl Ord for DecompressedPruningSeed {
231 fn cmp(&self, other: &Self) -> Ordering {
232 match self.log_stripes.cmp(&other.log_stripes) {
235 Ordering::Equal => self.stripe.cmp(&other.stripe),
236 ord => ord,
237 }
238 }
239}
240
241impl DecompressedPruningSeed {
242 pub const fn new(stripe: u32, log_stripes: u32) -> Result<Self, PruningError> {
267 if log_stripes > PRUNING_SEED_LOG_STRIPES_MASK {
268 Err(PruningError::LogStripesOutOfRange)
269 } else if !(stripe > 0 && stripe <= (1 << log_stripes)) {
270 Err(PruningError::StripeOutOfRange)
271 } else {
272 Ok(Self {
273 log_stripes,
274 stripe,
275 })
276 }
277 }
278
279 pub const fn decompress(seed: u32) -> Result<Option<Self>, PruningError> {
285 if seed == 0 {
286 return Ok(None);
288 }
289
290 let log_stripes = (seed >> PRUNING_SEED_LOG_STRIPES_SHIFT) & PRUNING_SEED_LOG_STRIPES_MASK;
291 let stripe = 1 + ((seed >> PRUNING_SEED_STRIPE_SHIFT) & PRUNING_SEED_STRIPE_MASK);
292
293 if stripe > (1 << log_stripes) {
294 return Err(PruningError::StripeOutOfRange);
295 }
296
297 Ok(Some(Self {
298 log_stripes,
299 stripe,
300 }))
301 }
302
303 pub const fn compress(&self) -> u32 {
305 (self.log_stripes << PRUNING_SEED_LOG_STRIPES_SHIFT)
306 | ((self.stripe - 1) << PRUNING_SEED_STRIPE_SHIFT)
307 }
308
309 pub const fn has_full_block(&self, height: usize, blockchain_height: usize) -> bool {
311 match get_block_pruning_stripe(height, blockchain_height, self.log_stripes) {
312 Some(block_stripe) => self.stripe == block_stripe,
313 None => true,
314 }
315 }
316
317 pub const fn get_next_unpruned_block(
331 &self,
332 block_height: usize,
333 blockchain_height: usize,
334 ) -> Result<usize, PruningError> {
335 if block_height > MAX_BLOCK_HEIGHT_USIZE || block_height > blockchain_height {
336 return Err(PruningError::BlockHeightTooLarge);
337 }
338
339 if blockchain_height > MAX_BLOCK_HEIGHT_USIZE {
340 return Err(PruningError::BlockChainHeightTooLarge);
341 }
342
343 if block_height + CRYPTONOTE_PRUNING_TIP_BLOCKS >= blockchain_height {
344 return Ok(block_height);
347 }
348
349 let block_pruning_stripe = get_block_pruning_stripe(block_height, blockchain_height, self.log_stripes)
350 .expect("We just checked if `block_height + CRYPTONOTE_PRUNING_TIP_BLOCKS >= blockchain_height`");
351 if self.stripe == block_pruning_stripe {
352 return Ok(block_height);
355 }
356
357 let cycles = (block_height / CRYPTONOTE_PRUNING_STRIPE_SIZE) >> self.log_stripes;
360 let cycles_start = cycles
363 + if self.stripe > block_pruning_stripe {
364 0
365 } else {
366 1
367 };
368
369 let calculated_height = cycles_start * (CRYPTONOTE_PRUNING_STRIPE_SIZE << self.log_stripes)
371 + (self.stripe as usize - 1) * CRYPTONOTE_PRUNING_STRIPE_SIZE;
372
373 if calculated_height + CRYPTONOTE_PRUNING_TIP_BLOCKS > blockchain_height {
374 Ok(blockchain_height.saturating_sub(CRYPTONOTE_PRUNING_TIP_BLOCKS))
376 } else if calculated_height < block_height {
377 Err(PruningError::CalculatedHeightSmallerThanEnteredBlock)
378 } else {
379 Ok(calculated_height)
380 }
381 }
382
383 pub fn get_next_pruned_block(
397 &self,
398 block_height: usize,
399 blockchain_height: usize,
400 ) -> Result<Option<usize>, PruningError> {
401 if block_height + CRYPTONOTE_PRUNING_TIP_BLOCKS >= blockchain_height {
402 return Ok(None);
405 }
406
407 let block_pruning_stripe = get_block_pruning_stripe(block_height, blockchain_height, self.log_stripes)
408 .expect("We just checked if `block_height + CRYPTONOTE_PRUNING_TIP_BLOCKS >= blockchain_height`");
409 if self.stripe != block_pruning_stripe {
410 return Ok(Some(block_height));
412 }
413
414 let next_stripe = 1 + (self.stripe & ((1 << self.log_stripes) - 1));
417 let seed = Self::new(next_stripe, self.log_stripes)
418 .expect("We just made sure this stripe is in range for this log_stripe");
419
420 let calculated_height = seed.get_next_unpruned_block(block_height, blockchain_height)?;
421
422 if calculated_height + CRYPTONOTE_PRUNING_TIP_BLOCKS > blockchain_height {
423 Ok(None)
425 } else {
426 Ok(Some(calculated_height))
427 }
428 }
429}
430
431const fn get_block_pruning_stripe(
432 block_height: usize,
433 blockchain_height: usize,
434 log_stripe: u32,
435) -> Option<u32> {
436 if block_height + CRYPTONOTE_PRUNING_TIP_BLOCKS >= blockchain_height {
437 None
438 } else {
439 #[expect(
440 clippy::cast_possible_truncation,
441 clippy::cast_sign_loss,
442 reason = "it's trivial to prove it's ok to us `as` here"
443 )]
444 Some(
445 (((block_height / CRYPTONOTE_PRUNING_STRIPE_SIZE) & ((1 << log_stripe) as usize - 1))
446 + 1) as u32,
447 )
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 fn make_all_pruning_seeds() -> Vec<PruningSeed> {
456 let possible_stripes = 1..=(1 << CRYPTONOTE_PRUNING_LOG_STRIPES);
457 possible_stripes
458 .map(|stripe| PruningSeed::new_pruned(stripe, CRYPTONOTE_PRUNING_LOG_STRIPES).unwrap())
459 .collect()
460 }
461
462 #[test]
463 fn from_u32_for_pruning_seed() {
464 let good_seeds = 384..=391;
465 for seed in good_seeds {
466 assert!(PruningSeed::decompress(seed).is_ok());
467 }
468 let bad_seeds = [383, 392];
469 for seed in bad_seeds {
470 assert!(PruningSeed::decompress(seed).is_err());
471 }
472 }
473
474 #[test]
475 fn make_invalid_pruning_seeds() {
476 let invalid_stripes = [0, (1 << CRYPTONOTE_PRUNING_LOG_STRIPES) + 1];
477
478 for stripe in invalid_stripes {
479 assert!(PruningSeed::new_pruned(stripe, CRYPTONOTE_PRUNING_LOG_STRIPES).is_err());
480 }
481 }
482
483 #[test]
484 fn get_pruning_log_stripe() {
485 let all_valid_seeds = make_all_pruning_seeds();
486 for seed in &all_valid_seeds {
487 assert_eq!(seed.get_log_stripes().unwrap(), 3);
488 }
489 }
490
491 #[test]
492 fn get_pruning_stripe() {
493 let all_valid_seeds = make_all_pruning_seeds();
494 #[expect(clippy::cast_possible_truncation)]
495 for (i, seed) in all_valid_seeds.iter().enumerate() {
496 assert_eq!(seed.get_stripe().unwrap(), i as u32 + 1);
497 }
498 }
499
500 #[test]
501 fn blocks_pruning_stripe() {
502 let blockchain_height = 76437863;
503
504 for i in 0_u32..8 {
505 assert_eq!(
506 get_block_pruning_stripe(
507 (i * 4096) as usize,
508 blockchain_height,
509 CRYPTONOTE_PRUNING_LOG_STRIPES
510 )
511 .unwrap(),
512 i + 1
513 );
514 }
515
516 for i in 0_u32..8 {
517 assert_eq!(
518 get_block_pruning_stripe(
519 32768 + (i * 4096) as usize,
520 blockchain_height,
521 CRYPTONOTE_PRUNING_LOG_STRIPES
522 )
523 .unwrap(),
524 i + 1
525 );
526 }
527
528 for i in 1_u32..8 {
529 assert_eq!(
530 get_block_pruning_stripe(
531 32767 + (i * 4096) as usize,
532 blockchain_height,
533 CRYPTONOTE_PRUNING_LOG_STRIPES
534 )
535 .unwrap(),
536 i
537 );
538 }
539
540 assert!(get_block_pruning_stripe(
542 blockchain_height - 5500,
543 blockchain_height,
544 CRYPTONOTE_PRUNING_LOG_STRIPES
545 )
546 .is_none());
547 }
548
549 #[test]
550 fn next_unpruned_block() {
551 let all_valid_seeds = make_all_pruning_seeds();
552 let blockchain_height = 76437863;
553
554 for (i, seed) in all_valid_seeds.iter().enumerate() {
555 assert_eq!(
556 seed.get_next_unpruned_block(0, blockchain_height).unwrap(),
557 i * 4096
558 );
559 }
560
561 for (i, seed) in all_valid_seeds.iter().enumerate() {
562 assert_eq!(
563 seed.get_next_unpruned_block((i + 1) * 4096, blockchain_height)
564 .unwrap(),
565 i * 4096 + 32768
566 );
567 }
568
569 for (i, seed) in all_valid_seeds.iter().enumerate() {
570 assert_eq!(
571 seed.get_next_unpruned_block((i + 8) * 4096, blockchain_height)
572 .unwrap(),
573 i * 4096 + 32768
574 );
575 }
576
577 for seed in &all_valid_seeds {
578 assert_eq!(
579 seed.get_next_unpruned_block(76437863 - 1, blockchain_height)
580 .unwrap(),
581 76437863 - 1
582 );
583 }
584
585 let zero_seed = PruningSeed::NotPruned;
586
587 assert_eq!(
588 zero_seed.get_next_unpruned_block(33443, 5565445).unwrap(),
589 33443
590 );
591
592 let seed = PruningSeed::decompress(384).unwrap();
593
594 assert_eq!(seed.get_next_unpruned_block(5000, 11000).unwrap(), 5500);
596 }
597
598 #[test]
599 fn next_pruned_block() {
600 let all_valid_seeds = make_all_pruning_seeds();
601 let blockchain_height = 76437863;
602
603 for seed in all_valid_seeds.iter().skip(1) {
604 assert_eq!(
605 seed.get_next_pruned_block(0, blockchain_height)
606 .unwrap()
607 .unwrap(),
608 0
609 );
610 }
611
612 for (i, seed) in all_valid_seeds.iter().enumerate() {
613 assert_eq!(
614 seed.get_next_pruned_block((i + 1) * 4096, blockchain_height)
615 .unwrap()
616 .unwrap(),
617 (i + 1) * 4096
618 );
619 }
620
621 for (i, seed) in all_valid_seeds.iter().enumerate() {
622 assert_eq!(
623 seed.get_next_pruned_block((i + 8) * 4096, blockchain_height)
624 .unwrap()
625 .unwrap(),
626 (i + 9) * 4096
627 );
628 }
629
630 for seed in &all_valid_seeds {
631 assert_eq!(
632 seed.get_next_pruned_block(76437863 - 1, blockchain_height)
633 .unwrap(),
634 None
635 );
636 }
637
638 let zero_seed = PruningSeed::NotPruned;
639
640 assert_eq!(
641 zero_seed.get_next_pruned_block(33443, 5565445).unwrap(),
642 None
643 );
644
645 let seed = PruningSeed::decompress(384).unwrap();
646
647 assert_eq!(seed.get_next_pruned_block(5000, 10000).unwrap(), None);
649 }
650}