pub const HASHRATE_DECIMALS: u8 = 2;
pub const HASHRATE_PER_TICKET: u64 = 100;
pub const LOYALTY_WEIGHT: u64 = 1;
pub const SKILL_WEIGHT: u64 = 1;
pub fn get_hashrate_ui_amount(hashrate: u64) -> String {
format!("{}.{:02}", hashrate / HASHRATE_PER_TICKET, hashrate % HASHRATE_PER_TICKET)
}
pub fn get_hashrate_tickets_count(hashrate: u64) -> u64 {
hashrate / HASHRATE_PER_TICKET
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct HashrateReward {
pub loyalty_points: u64,
pub skill_points: u64,
pub total: u64,
}
#[derive(Debug, thiserror::Error)]
pub enum HashrateRewardError {
#[error("hashrate reward overflows u64")]
MathOverflow,
}
pub fn hashrate_reward(
stake: u64,
streak: u32,
covered: u32,
total_tiles: u32,
usd_unit: u64,
) -> Result<HashrateReward, HashrateRewardError> {
if stake == 0 || covered == 0 {
return Ok(HashrateReward::default());
}
let covered = covered as u128;
let denom = covered * usd_unit as u128;
let base = stake as u128;
let skill_num = base * (SKILL_WEIGHT as u128 * total_tiles as u128);
let loyalty_num = base * (LOYALTY_WEIGHT as u128 * streak as u128 * covered);
let total: u64 = ((loyalty_num + skill_num) / denom)
.try_into()
.map_err(|_| HashrateRewardError::MathOverflow)?;
let skill_points: u64 = (skill_num / denom) as u64;
let loyalty_points = total - skill_points;
Ok(HashrateReward {
loyalty_points,
skill_points,
total,
})
}
#[cfg(test)]
mod tests {
use super::*;
const USD_UNIT: u64 = 1_000_000; const N: u32 = 21;
#[test]
fn worked_example_two_dollars_streak_15_three_tiles() {
let r = hashrate_reward(2_000_000, 15, 3, N, USD_UNIT).unwrap();
assert_eq!(r.total, 44);
}
#[test]
fn channels_sum_to_total() {
let r = hashrate_reward(2_000_000, 15, 3, N, USD_UNIT).unwrap();
assert_eq!(r.loyalty_points + r.skill_points, r.total);
}
#[test]
fn full_coverage_has_no_skill_bonus_beyond_floor() {
let r = hashrate_reward(1_000_000, 1, N, N, USD_UNIT).unwrap();
assert_eq!(r.total, 2);
assert_eq!(r.skill_points, 1);
assert_eq!(r.loyalty_points, 1);
}
#[test]
fn single_tile_is_max_conviction() {
let r = hashrate_reward(1_000_000, 1, 1, N, USD_UNIT).unwrap();
assert_eq!(r.total, 22);
}
#[test]
fn ceiling_multiplier_per_dollar() {
let r = hashrate_reward(1_000_000, 30, 1, N, USD_UNIT).unwrap();
assert_eq!(r.total, 51);
}
#[test]
fn one_dollar_at_max_streak_earns_1_01_points() {
let r = hashrate_reward(1_000_000, 100, N, N, USD_UNIT).unwrap();
assert_eq!(r.total, 101);
assert_eq!(HASHRATE_PER_TICKET, 10u64.pow(HASHRATE_DECIMALS as u32));
}
#[test]
fn zero_stake_earns_nothing() {
let r = hashrate_reward(0, 30, 1, N, USD_UNIT).unwrap();
assert_eq!(r, HashrateReward::default());
}
#[test]
fn zero_coverage_earns_nothing_without_dividing_by_zero() {
let r = hashrate_reward(1_000_000, 5, 0, N, USD_UNIT).unwrap();
assert_eq!(r, HashrateReward::default());
}
#[test]
fn sub_unit_stake_floors_to_zero() {
let r = hashrate_reward(10_000, 1, N, N, USD_UNIT).unwrap();
assert_eq!(r.total, 0);
}
#[test]
fn ui_amount_formats_raw_units_with_two_decimals() {
assert_eq!(get_hashrate_ui_amount(101), "1.01");
assert_eq!(get_hashrate_ui_amount(0), "0.00");
assert_eq!(get_hashrate_ui_amount(5), "0.05");
assert_eq!(get_hashrate_ui_amount(230), "2.30");
assert_eq!(get_hashrate_ui_amount(u64::MAX), "184467440737095516.15");
}
#[test]
fn tickets_count_floors_raw_units_to_whole_tickets() {
assert_eq!(get_hashrate_tickets_count(0), 0);
assert_eq!(get_hashrate_tickets_count(99), 0);
assert_eq!(get_hashrate_tickets_count(100), 1);
assert_eq!(get_hashrate_tickets_count(6_550), 65);
assert_eq!(get_hashrate_tickets_count(u64::MAX), u64::MAX / 100);
}
#[test]
fn matches_next_streak_multiplier_for_an_upcoming_deploy() {
let streak = crate::next_streak_multiplier(4, 5, 6);
assert_eq!(streak, 5);
let r = hashrate_reward(1_000_000, streak, N, N, USD_UNIT).unwrap();
assert_eq!(r.total, 6);
}
}