use std::collections::BTreeSet;
use zcash_protocol::consensus::BlockHeight;
pub use zcash_protocol::zip318::AnchorBucketInterval as AnchorRetentionInterval;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AnchorRetention {
from_height: BlockHeight,
intervals: BTreeSet<AnchorRetentionInterval>,
}
impl AnchorRetention {
pub fn new(from_height: BlockHeight, interval: AnchorRetentionInterval) -> Self {
Self {
from_height,
intervals: core::iter::once(interval).collect(),
}
}
pub fn union(
from_height: BlockHeight,
intervals: impl IntoIterator<Item = AnchorRetentionInterval>,
) -> Option<Self> {
let intervals: BTreeSet<AnchorRetentionInterval> = intervals.into_iter().collect();
(!intervals.is_empty()).then_some(Self {
from_height,
intervals,
})
}
pub fn from_height(&self) -> BlockHeight {
self.from_height
}
pub fn intervals(&self) -> &BTreeSet<AnchorRetentionInterval> {
&self.intervals
}
pub fn retains(&self, height: BlockHeight) -> bool {
height >= self.from_height && self.intervals.iter().any(|i| i.is_boundary(height))
}
pub fn retained_in_range(
&self,
range: core::ops::RangeInclusive<BlockHeight>,
) -> BTreeSet<BlockHeight> {
let start = u64::from(u32::from(core::cmp::max(*range.start(), self.from_height)));
let end = u64::from(u32::from(*range.end()));
self.intervals
.iter()
.flat_map(|interval| {
let step = u64::from(interval.block_count().get());
let first = start.div_ceil(step) * step;
(0u64..)
.map(move |k| first + k * step)
.take_while(move |boundary| *boundary <= end)
.map(|boundary| {
BlockHeight::from_u32(
u32::try_from(boundary).expect("bounded by a u32 height"),
)
})
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::num::NonZeroU32;
use proptest::prelude::*;
fn interval(blocks: u32) -> AnchorRetentionInterval {
AnchorRetentionInterval::custom(NonZeroU32::new(blocks).expect("nonzero"))
}
#[test]
fn union_of_no_intervals_is_none() {
assert!(AnchorRetention::union(BlockHeight::from_u32(0), []).is_none());
assert!(
AnchorRetention::union(BlockHeight::from_u32(0), [AnchorRetentionInterval::ZIP_318])
.is_some()
);
}
#[test]
fn zip_318_is_the_default() {
assert_eq!(AnchorRetentionInterval::default(), interval(144));
assert_eq!(AnchorRetentionInterval::ZIP_318.block_count().get(), 144);
}
#[test]
fn zip_318_boundaries() {
let i = AnchorRetentionInterval::ZIP_318;
for (height, below, above) in [
(0u32, 0u32, 0u32),
(1, 0, 144),
(143, 0, 144),
(144, 144, 144),
(145, 144, 288),
(1_000_000, 999_936, 1_000_080),
] {
let h = BlockHeight::from_u32(height);
assert_eq!(
u32::from(i.boundary_at_or_below(h)),
below,
"below {height}"
);
assert_eq!(
u32::from(i.boundary_at_or_above(h)),
above,
"above {height}"
);
}
assert!(i.is_boundary(BlockHeight::from_u32(288)));
assert!(!i.is_boundary(BlockHeight::from_u32(289)));
}
#[test]
fn retained_in_range_examples() {
let h = BlockHeight::from_u32;
let heights = |policy: &AnchorRetention, lo: u32, hi: u32| {
policy
.retained_in_range(h(lo)..=h(hi))
.into_iter()
.map(u32::from)
.collect::<Vec<_>>()
};
let single = AnchorRetention::new(h(25), interval(10));
assert_eq!(heights(&single, 15, 55), vec![30, 40, 50]);
assert_eq!(heights(&single, 30, 50), vec![30, 40, 50]);
assert_eq!(heights(&single, 50, 30), Vec::<u32>::new());
assert_eq!(heights(&single, 0, 24), Vec::<u32>::new());
let union = AnchorRetention::union(h(0), [interval(10), interval(15)]).expect("non-empty");
assert_eq!(heights(&union, 1, 45), vec![10, 15, 20, 30, 40, 45]);
}
proptest! {
#[test]
fn boundary_rounding_props(h in 0u32..5_000_000, blocks in 1u32..10_000) {
let i = interval(blocks);
let height = BlockHeight::from_u32(h);
let below = u32::from(i.boundary_at_or_below(height));
prop_assert!(i.is_boundary(BlockHeight::from_u32(below)));
prop_assert!(below <= h);
prop_assert!(h - below < blocks);
let above = u32::from(i.boundary_at_or_above(height));
prop_assert!(i.is_boundary(BlockHeight::from_u32(above)));
prop_assert!(above >= h);
prop_assert!(above - h < blocks);
}
#[test]
fn is_boundary_agrees_with_rounding(h in 0u32..5_000_000, blocks in 1u32..10_000) {
let i = interval(blocks);
let height = BlockHeight::from_u32(h);
prop_assert_eq!(i.is_boundary(height), i.boundary_at_or_below(height) == height);
prop_assert_eq!(i.is_boundary(height), i.boundary_at_or_above(height) == height);
}
#[test]
fn retention_policy_props(
floor in 0u32..1_000_000,
offset in 0u32..2_000,
blocks in 1u32..500,
) {
let i = interval(blocks);
let policy = AnchorRetention::new(BlockHeight::from_u32(floor), i);
let h = BlockHeight::from_u32(floor.saturating_sub(1_000).saturating_add(offset));
prop_assert_eq!(policy.retains(h), h >= BlockHeight::from_u32(floor) && i.is_boundary(h));
}
#[test]
fn retained_in_range_agrees_with_retains(
floor in 0u32..100_000,
lo in 0u32..200_000,
len in 0u32..2_000,
a in 1u32..500,
b in 1u32..500,
) {
let f = BlockHeight::from_u32(floor);
let policy = AnchorRetention::union(f, [interval(a), interval(b)]).expect("non-empty");
let (lo, hi) = (BlockHeight::from_u32(lo), BlockHeight::from_u32(lo + len));
let enumerated = policy.retained_in_range(lo..=hi);
for h in u32::from(lo)..=u32::from(hi) {
let h = BlockHeight::from_u32(h);
prop_assert_eq!(
enumerated.contains(&h),
policy.retains(h),
"height {:?}", h
);
}
for h in &enumerated {
prop_assert!((lo..=hi).contains(h));
}
}
#[test]
fn union_retains_every_constituent_grid(
floor in 0u32..100_000,
offset in 0u32..5_000,
a in 1u32..500,
b in 1u32..500,
) {
let (ia, ib) = (interval(a), interval(b));
let f = BlockHeight::from_u32(floor);
let union = AnchorRetention::union(f, [ia, ib]).expect("non-empty");
let h = BlockHeight::from_u32(floor.saturating_add(offset));
prop_assert_eq!(
union.retains(h),
AnchorRetention::new(f, ia).retains(h) || AnchorRetention::new(f, ib).retains(h)
);
for single in [ia, ib] {
if AnchorRetention::new(f, single).retains(h) {
prop_assert!(union.retains(h));
}
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PoolMigrationParams {
interval: AnchorRetentionInterval,
}
impl PoolMigrationParams {
pub fn new(interval: AnchorRetentionInterval) -> Self {
Self { interval }
}
}
impl From<AnchorRetentionInterval> for PoolMigrationParams {
fn from(interval: AnchorRetentionInterval) -> Self {
Self::new(interval)
}
}
impl zcash_protocol::zip318::PoolMigrationConstants for PoolMigrationParams {
fn anchor_bucket_interval(&self) -> AnchorRetentionInterval {
self.interval
}
}