Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions controller/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ version.workspace = true
[dependencies]
const-crypto = { workspace = true }
generic-array-struct = { workspace = true }
sanctum-fee-ratio = { workspace = true }
sanctum-u64-ratio = { workspace = true }

[dev-dependencies]
borsh = { workspace = true, features = ["derive", "std"] }
Expand Down
1 change: 1 addition & 0 deletions controller/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ pub mod instructions;
pub mod keys;
pub mod pda;
pub mod typedefs;
pub mod yield_release;

keys::id_str!(ID_STR, ID, "5ocnV1qiCgaQR8Jb8xWnVbApfaygJ8tNoZfgPwsgx9kx");
2 changes: 2 additions & 0 deletions controller/core/src/typedefs/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pub mod lst_state;
pub mod rps;
pub mod u8bool;
pub mod uq0_64;
84 changes: 84 additions & 0 deletions controller/core/src/typedefs/rps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use core::{error::Error, fmt::Display, ops::Deref};

use crate::typedefs::uq0_64::UQ0_64;

const MIN_RPS_RAW: u64 = 18_446_744;

/// Approx one pico (1 / 1_000_000_000_000)
pub const MIN_RPS: UQ0_64 = UQ0_64(MIN_RPS_RAW);

/// Proportion of withheld_lamports to release per slot
#[repr(transparent)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Rps(UQ0_64);

impl Rps {
pub const MIN: Self = Self(MIN_RPS);

#[inline]
pub const fn new(raw: UQ0_64) -> Result<Self, RpsTooSmallErr> {
// have to use .0 to use primitive const < operator
if raw.0 < MIN_RPS.0 {
Err(RpsTooSmallErr { actual: raw })
} else {
Ok(Self(raw))
}
}

#[inline]
pub const fn as_inner(&self) -> &UQ0_64 {
&self.0
}
}

impl Deref for Rps {
type Target = UQ0_64;

#[inline]
fn deref(&self) -> &Self::Target {
self.as_inner()
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RpsTooSmallErr {
pub actual: UQ0_64,
}

impl Display for RpsTooSmallErr {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let Self { actual } = self;
f.write_fmt(format_args!("{actual} < {MIN_RPS} (min)"))
}
}

impl Error for RpsTooSmallErr {}

#[cfg(test)]
pub mod test_utils {
use proptest::prelude::*;

use super::*;

pub fn any_rps_strat() -> impl Strategy<Value = Rps> {
(MIN_RPS_RAW..=u64::MAX)
.prop_map(UQ0_64)
.prop_map(Rps::new)
.prop_map(Result::unwrap)
}
}

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

#[test]
fn rps_new_sc() {
const FAIL: UQ0_64 = UQ0_64(MIN_RPS_RAW - 1);
const SUCC: UQ0_64 = UQ0_64(MIN_RPS_RAW);

assert_eq!(Rps::new(FAIL), Err(RpsTooSmallErr { actual: FAIL }));
assert_eq!(Rps::new(SUCC), Ok(Rps(SUCC)));
}
}
235 changes: 235 additions & 0 deletions controller/core/src/typedefs/uq0_64.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
//! Binary fixed-point Q numbers
//! (https://en.wikipedia.org/wiki/Q_(number_format))
//!
//! ## Why handroll our own?
//! - `fixed` crate has dependencies that we dont need
//! - we only need multiplication and exponentiation of unsigned ratios <= 1.0
//!
//! ### TODO
//! Consider generalizing and separating this out into its own crate?

use core::{fmt::Display, ops::Mul};

use sanctum_u64_ratio::Ratio;

/// 64-bit fraction only fixed-point number to represent a value between 0.0 and 1.0
/// (denominator = u64::MAX)
#[repr(transparent)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct UQ0_64(pub u64);

#[inline]
pub const fn uq0_64_mul(UQ0_64(a): UQ0_64, UQ0_64(b): UQ0_64) -> UQ0_64 {
const ROUNDING_BIAS: u128 = 1 << 63;

// where d = u64::MAX
// (n1/d) * (n2/d) = n1*n2/d^2
// as-safety: u128 bitwidth > u64 bitwidth
// unchecked arith safety: u64*u64 never overflows u128
let res = (a as u128) * (b as u128);
// round off 64th bit
// unchecked arith safety: res <= u64::MAX * u64::MAX + ROUNDING_BIAS < u128::MAX
let rounded = res + ROUNDING_BIAS;
// convert back to UQ0_64 by making denom = d
// n1*n2/d^2 = (n1*n2/d) / d
// so we need to divide res by d,
// and division by u64::MAX is just >> 64
// as-safety: truncating conversion is what we want
// to achieve floor mul
UQ0_64((rounded >> 64) as u64)
}

#[inline]
pub const fn uq0_64_into_ratio(a: UQ0_64) -> Ratio<u64, u64> {
Ratio {
n: a.0,
d: u64::MAX,
}
}

#[inline]
pub const fn uq0_64_pow(mut base: UQ0_64, mut exp: u64) -> UQ0_64 {
// sq & mul
let mut res = UQ0_64::ONE;
while exp > 0 {
if exp % 2 == 1 {
res = uq0_64_mul(res, base);
}
base = uq0_64_mul(base, base);
exp /= 2;
}
res
}

impl UQ0_64 {
pub const ZERO: Self = Self(0);
pub const ONE: Self = Self(u64::MAX);

/// Rounding is to closest bit
#[inline]
pub const fn const_mul(a: Self, b: Self) -> Self {
uq0_64_mul(a, b)
}

/// Returns `1.0 - self`
#[inline]
pub const fn one_minus(self) -> Self {
// unchecked-arith safety: self.0 <= u64::MAX
Self(u64::MAX - self.0)
}

#[inline]
pub const fn pow(self, exp: u64) -> Self {
uq0_64_pow(self, exp)
}

#[inline]
pub const fn into_ratio(self) -> Ratio<u64, u64> {
uq0_64_into_ratio(self)
}
}

impl Mul for UQ0_64 {
type Output = Self;

/// Rounding floors
#[inline]
fn mul(self, rhs: Self) -> Self::Output {
Self::const_mul(self, rhs)
}
}

impl From<UQ0_64> for Ratio<u64, u64> {
#[inline]
fn from(v: UQ0_64) -> Self {
v.into_ratio()
}
}

impl Display for UQ0_64 {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.into_ratio().fmt(f)
}
}

#[cfg(test)]
mod tests {
use core::cmp::min;

use expect_test::expect;
use proptest::prelude::*;
use sanctum_u64_ratio::Ratio;

use super::*;

const D: f64 = u64::MAX as f64;

/// max error bounds for multiplication
/// - UQ0_64. 1-bit, so 2^-64
/// - f64 for range 0.0-1.0, around 2^-54 (around 2^10 larger than UQ0_64 because fewer bits dedicated to fraction)
const MAX_MUL_DIFF_F64_VS_US: u64 = 4096;

const EPSILON_RATIO_DIFF: Ratio<u64, u64> = Ratio {
n: 1,
d: 1_000_000_000_000,
};

const fn f64_approx(UQ0_64(a): UQ0_64) -> f64 {
(a as f64) / D
}

fn uq0_64_approx(a: f64) -> UQ0_64 {
if a > 1.0 {
panic!("a={a} > 1.0");
}
UQ0_64((a * D).floor() as u64)
}

proptest! {
#[test]
fn mul_pt(
[a, b] in [any::<u64>(); 2].map(|s| s.prop_map(UQ0_64))
) {
let us = a * b;

// a*b <= a and <= b since both are <= 1.0
prop_assert!(us <= a);
prop_assert!(us <= b);

let approx_f64 = [a, b].map(f64_approx).into_iter().reduce(core::ops::Mul::mul).unwrap();
let approx_uq0_64 = uq0_64_approx(approx_f64);

// small error from f64 result
let diff_u64 = us.0.abs_diff(approx_uq0_64.0);
prop_assert!(
diff_u64 <= MAX_MUL_DIFF_F64_VS_US,
"{}, {}",
us.0,
approx_uq0_64.0
);

// diff should not exceed epsilon proportion of value
let diff_r = Ratio {
n: diff_u64,
d: min(us.0, approx_uq0_64.0),
};
prop_assert!(diff_r < EPSILON_RATIO_DIFF, "diff_r: {diff_r}");
}
}

proptest! {
#[test]
fn exp_pt(base in any::<u64>().prop_map(UQ0_64), exp: u64) {
let us = base.pow(exp);

// (base)^+ve should be <= base since base <= 1.0
prop_assert!(us <= base);

let approx_f64 = f64_approx(us).powf(exp as f64);
let approx_uq0_64 = uq0_64_approx(approx_f64);

// small error from f64 result
let diff_u64 = us.0.abs_diff(approx_uq0_64.0);
// same err bound as mul_pt since a.pow(2) = a * a
prop_assert!(
diff_u64 <= MAX_MUL_DIFF_F64_VS_US,
"{}, {}",
us.0,
approx_uq0_64.0
);

// diff should not exceed epsilon proportion of value
let diff_r = Ratio {
n: diff_u64,
d: min(us.0, approx_uq0_64.0),
};
prop_assert!(diff_r < EPSILON_RATIO_DIFF, "diff_r: {diff_r}");

// exponent of anything < 1.0 eventually reaches 0
if base != UQ0_64::ONE {
prop_assert_eq!(base.pow(u64::MAX), UQ0_64::ZERO);
}

// compare against naive multiplication implementation
const LIM: u64 = u16::MAX as u64;
let naive_mul_res = match exp {
0 => UQ0_64::ONE,
1..=LIM => (0..exp).fold(base, |res, _| res * base),
_will_take_too_long_to_run => return Ok(())
};
prop_assert_eq!(naive_mul_res, us);
}
}

#[test]
fn into_ratio_sc() {
expect![[r#"
Ratio {
n: 9223372036854775807,
d: 18446744073709551615,
}
"#]]
.assert_debug_eq(&UQ0_64(u64::MAX / 2).into_ratio());
}
}
Loading