use core::num::NonZeroU32;
use crate::consensus::BlockHeight;
use crate::value::{COIN, Zatoshis};
const DENOMINATION_RADIX: u64 = 10;
const ONE_TWO_FIVE_DESCENDING: [u64; 3] = [5, 2, 1];
pub const DENOM_CAP: Zatoshis = Zatoshis::const_from_u64(10_000 * COIN);
pub const MAX_RESIDUAL_VALUE: Zatoshis = Zatoshis::const_from_u64(COIN / 100);
pub const PREP_TX_ACTIONS: usize = 16;
pub const TRANSFER_DELAY_MEAN: NonZeroU32 = NonZeroU32::new(144).expect("144 is nonzero");
pub const TRANSFER_DELAY_CAP: NonZeroU32 = NonZeroU32::new(576).expect("576 is nonzero");
pub const DELAY_CAP_RATIO: NonZeroU32 = NonZeroU32::new(4).expect("4 is nonzero");
pub const ANCHOR_AGE_CAP: u32 = 16;
pub const EXPIRY_MODULUS: u32 = 34_560;
pub const EXPIRY_WINDOW: u32 = 2 * EXPIRY_MODULUS;
pub fn largest_one_two_five(hi: u64, floor: u64) -> u64 {
if hi < floor {
return 0;
}
let mut pow = floor;
while pow.checked_mul(DENOMINATION_RADIX).is_some_and(|p| p <= hi) {
pow *= DENOMINATION_RADIX;
}
for multiple in ONE_TWO_FIVE_DESCENDING {
if let Some(v) = pow.checked_mul(multiple)
&& v <= hi
{
return v;
}
}
pow
}
fn is_canonical_within(value: Zatoshis, min: Zatoshis, max: Zatoshis) -> bool {
if value < min || value > max {
return false;
}
let mut n = u64::from(value);
while n.is_multiple_of(DENOMINATION_RADIX) {
n /= DENOMINATION_RADIX;
}
ONE_TWO_FIVE_DESCENDING.contains(&n)
}
pub fn is_canonical_denomination(value: Zatoshis) -> bool {
is_canonical_within(value, MAX_RESIDUAL_VALUE, DENOM_CAP)
}
pub fn expiry_height(target_height: BlockHeight) -> BlockHeight {
let h = u32::from(target_height);
BlockHeight::from_u32(h - (h % EXPIRY_MODULUS)) + EXPIRY_WINDOW
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AnchorBucketInterval(NonZeroU32);
impl AnchorBucketInterval {
pub const ZIP_318: Self = Self(NonZeroU32::new(144).expect("144 is nonzero"));
pub const fn custom(blocks: NonZeroU32) -> Self {
Self(blocks)
}
pub fn block_count(&self) -> NonZeroU32 {
self.0
}
pub fn is_boundary(&self, height: BlockHeight) -> bool {
u32::from(height) % self.0 == 0
}
pub fn boundary_at_or_below(&self, height: BlockHeight) -> BlockHeight {
let h = u32::from(height);
BlockHeight::from_u32(h - (h % self.0))
}
pub fn boundary_at_or_above(&self, height: BlockHeight) -> BlockHeight {
let h = u32::from(height);
let r = h % self.0;
BlockHeight::from_u32(if r == 0 {
h
} else {
h.saturating_add(self.0.get() - r)
})
}
}
impl Default for AnchorBucketInterval {
fn default() -> Self {
Self::ZIP_318
}
}
pub trait PoolMigrationConstants {
fn anchor_bucket_interval(&self) -> AnchorBucketInterval {
AnchorBucketInterval::ZIP_318
}
fn denomination_cap(&self) -> Zatoshis {
DENOM_CAP
}
fn max_residual_value(&self) -> Zatoshis {
MAX_RESIDUAL_VALUE
}
fn preparation_tx_actions(&self) -> usize {
PREP_TX_ACTIONS
}
fn transfer_delay(&self) -> (NonZeroU32, NonZeroU32) {
(TRANSFER_DELAY_MEAN, TRANSFER_DELAY_CAP)
}
fn anchor_age_cap(&self) -> u32 {
ANCHOR_AGE_CAP
}
fn expiry_window(&self) -> (u32, u32) {
(EXPIRY_MODULUS, EXPIRY_WINDOW)
}
fn is_canonical_denomination(&self, value: Zatoshis) -> bool {
is_canonical_within(value, self.max_residual_value(), self.denomination_cap())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_denominations_are_the_one_two_five_series_in_range() {
for zat in [
1_000_000u64,
2_000_000,
5_000_000,
10_000_000,
100_000_000,
200_000_000,
1_000_000_000_000,
] {
assert!(
is_canonical_denomination(Zatoshis::const_from_u64(zat)),
"{zat} should be canonical"
);
}
for zat in [1u64, 2, 5, 100_000, 500_000] {
assert!(
!is_canonical_denomination(Zatoshis::const_from_u64(zat)),
"{zat} is below the minimum denomination"
);
}
assert!(!is_canonical_denomination(Zatoshis::const_from_u64(
2_000_000_000_000
)));
for zat in [3_000_000u64, 1_000_001, 999_999_999] {
assert!(
!is_canonical_denomination(Zatoshis::const_from_u64(zat)),
"{zat} is off the series"
);
}
assert!(!is_canonical_denomination(Zatoshis::ZERO));
}
#[test]
fn largest_at_or_below_picks_the_greatest_series_member() {
assert_eq!(largest_one_two_five(0, 1), 0);
assert_eq!(largest_one_two_five(4, 1), 2);
assert_eq!(largest_one_two_five(9, 1), 5);
assert_eq!(largest_one_two_five(10, 1), 10);
assert_eq!(largest_one_two_five(123_456, 1), 100_000);
assert_eq!(largest_one_two_five(999_999, 1_000_000), 0);
}
#[test]
fn zip_318_bounds_are_themselves_canonical() {
assert!(is_canonical_denomination(MAX_RESIDUAL_VALUE));
assert!(is_canonical_denomination(DENOM_CAP));
}
#[test]
fn zip_318_interval_is_144_blocks() {
assert_eq!(AnchorBucketInterval::ZIP_318.block_count().get(), 144);
assert_eq!(
AnchorBucketInterval::default(),
AnchorBucketInterval::ZIP_318
);
}
#[test]
fn boundaries_round_to_multiples_of_the_interval() {
let i = AnchorBucketInterval::ZIP_318;
for (height, below, above) in [
(0u32, 0u32, 0u32),
(1, 0, 144),
(143, 0, 144),
(144, 144, 144),
(145, 144, 288),
(2_000_000, 1_999_872, 2_000_016),
] {
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 expiry_is_shared_across_a_modulus_period() {
let period_start = 3 * EXPIRY_MODULUS;
let expected = BlockHeight::from_u32(period_start + EXPIRY_WINDOW);
for offset in [0u32, 1, EXPIRY_MODULUS / 2, EXPIRY_MODULUS - 1] {
let h = BlockHeight::from_u32(period_start + offset);
assert_eq!(
expiry_height(h),
expected,
"offset {offset} must share the expiry"
);
assert!(expiry_height(h) > h);
assert!(u32::from(expiry_height(h)) - u32::from(h) <= EXPIRY_WINDOW);
}
let next = BlockHeight::from_u32(period_start + EXPIRY_MODULUS);
assert_eq!(
expiry_height(next),
BlockHeight::from_u32(period_start + EXPIRY_MODULUS + EXPIRY_WINDOW)
);
}
#[test]
fn scheduling_constants_carry_the_zip_318_values() {
assert_eq!(PREP_TX_ACTIONS, 16);
assert_eq!(TRANSFER_DELAY_MEAN.get(), 144);
assert_eq!(TRANSFER_DELAY_CAP.get(), 576);
assert_eq!(
TRANSFER_DELAY_CAP.get(),
TRANSFER_DELAY_MEAN.get() * DELAY_CAP_RATIO.get()
);
assert_eq!(ANCHOR_AGE_CAP, 16);
assert_eq!(EXPIRY_MODULUS, 34_560);
assert_eq!(EXPIRY_WINDOW, 2 * EXPIRY_MODULUS);
}
#[test]
fn defaults_are_the_zip_318_values() {
#[derive(Clone)]
struct Specified;
impl PoolMigrationConstants for Specified {}
assert_eq!(
Specified.anchor_bucket_interval(),
AnchorBucketInterval::ZIP_318
);
assert_eq!(Specified.denomination_cap(), DENOM_CAP);
assert_eq!(Specified.max_residual_value(), MAX_RESIDUAL_VALUE);
assert_eq!(Specified.preparation_tx_actions(), PREP_TX_ACTIONS);
assert_eq!(
Specified.transfer_delay(),
(TRANSFER_DELAY_MEAN, TRANSFER_DELAY_CAP)
);
assert_eq!(Specified.anchor_age_cap(), ANCHOR_AGE_CAP);
assert_eq!(Specified.expiry_window(), (EXPIRY_MODULUS, EXPIRY_WINDOW));
}
#[test]
fn an_implementor_may_override_only_the_interval() {
#[derive(Clone)]
struct ShortGrid;
impl PoolMigrationConstants for ShortGrid {
fn anchor_bucket_interval(&self) -> AnchorBucketInterval {
AnchorBucketInterval::custom(NonZeroU32::new(12).expect("12 is nonzero"))
}
}
assert_eq!(ShortGrid.anchor_bucket_interval().block_count().get(), 12);
assert_eq!(ShortGrid.denomination_cap(), DENOM_CAP);
assert_eq!(ShortGrid.preparation_tx_actions(), PREP_TX_ACTIONS);
}
#[test]
fn overridden_bounds_narrow_the_canonical_set() {
#[derive(Clone)]
struct SmallCap;
impl PoolMigrationConstants for SmallCap {
fn denomination_cap(&self) -> Zatoshis {
Zatoshis::const_from_u64(COIN)
}
}
let two_zec = Zatoshis::const_from_u64(2 * COIN);
assert!(is_canonical_denomination(two_zec));
assert!(!SmallCap.is_canonical_denomination(two_zec));
assert!(SmallCap.is_canonical_denomination(Zatoshis::const_from_u64(COIN)));
}
}