1pub const HASHRATE_DECIMALS: u8 = 2;
4
5pub const HASHRATE_PER_TICKET: u64 = 100;
8
9pub const LOYALTY_WEIGHT: u64 = 1;
12
13pub const SKILL_WEIGHT: u64 = 1;
16
17pub fn get_hashrate_ui_amount(hashrate: u64) -> String {
21 format!("{}.{:02}", hashrate / HASHRATE_PER_TICKET, hashrate % HASHRATE_PER_TICKET)
22}
23
24pub fn get_hashrate_tickets_count(hashrate: u64) -> u64 {
27 hashrate / HASHRATE_PER_TICKET
28}
29
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
34pub struct HashrateReward {
35 pub loyalty_points: u64,
37 pub skill_points: u64,
39 pub total: u64,
41}
42
43#[derive(Debug, thiserror::Error)]
44pub enum HashrateRewardError {
45 #[error("hashrate reward overflows u64")]
46 MathOverflow,
47}
48
49pub fn hashrate_reward(
67 stake: u64,
68 streak: u32,
69 covered: u32,
70 total_tiles: u32,
71 usd_unit: u64,
72) -> Result<HashrateReward, HashrateRewardError> {
73 if stake == 0 || covered == 0 {
75 return Ok(HashrateReward::default());
76 }
77
78 let covered = covered as u128;
79 let denom = covered * usd_unit as u128;
80 let base = stake as u128;
81
82 let skill_num = base * (SKILL_WEIGHT as u128 * total_tiles as u128);
85 let loyalty_num = base * (LOYALTY_WEIGHT as u128 * streak as u128 * covered);
86
87 let total: u64 = ((loyalty_num + skill_num) / denom)
88 .try_into()
89 .map_err(|_| HashrateRewardError::MathOverflow)?;
90 let skill_points: u64 = (skill_num / denom) as u64;
93 let loyalty_points = total - skill_points;
94
95 Ok(HashrateReward {
96 loyalty_points,
97 skill_points,
98 total,
99 })
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 const USD_UNIT: u64 = 1_000_000; const N: u32 = 21;
108
109 #[test]
110 fn worked_example_two_dollars_streak_15_three_tiles() {
111 let r = hashrate_reward(2_000_000, 15, 3, N, USD_UNIT).unwrap();
113 assert_eq!(r.total, 44);
114 }
115
116 #[test]
117 fn channels_sum_to_total() {
118 let r = hashrate_reward(2_000_000, 15, 3, N, USD_UNIT).unwrap();
119 assert_eq!(r.loyalty_points + r.skill_points, r.total);
120 }
121
122 #[test]
123 fn full_coverage_has_no_skill_bonus_beyond_floor() {
124 let r = hashrate_reward(1_000_000, 1, N, N, USD_UNIT).unwrap();
127 assert_eq!(r.total, 2);
128 assert_eq!(r.skill_points, 1);
129 assert_eq!(r.loyalty_points, 1);
130 }
131
132 #[test]
133 fn single_tile_is_max_conviction() {
134 let r = hashrate_reward(1_000_000, 1, 1, N, USD_UNIT).unwrap();
137 assert_eq!(r.total, 22);
138 }
139
140 #[test]
141 fn ceiling_multiplier_per_dollar() {
142 let r = hashrate_reward(1_000_000, 30, 1, N, USD_UNIT).unwrap();
144 assert_eq!(r.total, 51);
145 }
146
147 #[test]
148 fn one_dollar_at_max_streak_earns_1_01_points() {
149 let r = hashrate_reward(1_000_000, 100, N, N, USD_UNIT).unwrap();
152 assert_eq!(r.total, 101);
153 assert_eq!(HASHRATE_PER_TICKET, 10u64.pow(HASHRATE_DECIMALS as u32));
154 }
155
156 #[test]
157 fn zero_stake_earns_nothing() {
158 let r = hashrate_reward(0, 30, 1, N, USD_UNIT).unwrap();
159 assert_eq!(r, HashrateReward::default());
160 }
161
162 #[test]
163 fn zero_coverage_earns_nothing_without_dividing_by_zero() {
164 let r = hashrate_reward(1_000_000, 5, 0, N, USD_UNIT).unwrap();
165 assert_eq!(r, HashrateReward::default());
166 }
167
168 #[test]
169 fn sub_unit_stake_floors_to_zero() {
170 let r = hashrate_reward(10_000, 1, N, N, USD_UNIT).unwrap();
172 assert_eq!(r.total, 0);
173 }
174
175 #[test]
176 fn ui_amount_formats_raw_units_with_two_decimals() {
177 assert_eq!(get_hashrate_ui_amount(101), "1.01");
178 assert_eq!(get_hashrate_ui_amount(0), "0.00");
179 assert_eq!(get_hashrate_ui_amount(5), "0.05");
180 assert_eq!(get_hashrate_ui_amount(230), "2.30");
181 assert_eq!(get_hashrate_ui_amount(u64::MAX), "184467440737095516.15");
182 }
183
184 #[test]
185 fn tickets_count_floors_raw_units_to_whole_tickets() {
186 assert_eq!(get_hashrate_tickets_count(0), 0);
187 assert_eq!(get_hashrate_tickets_count(99), 0);
188 assert_eq!(get_hashrate_tickets_count(100), 1);
189 assert_eq!(get_hashrate_tickets_count(6_550), 65);
190 assert_eq!(get_hashrate_tickets_count(u64::MAX), u64::MAX / 100);
191 }
192
193 #[test]
194 fn matches_next_streak_multiplier_for_an_upcoming_deploy() {
195 let streak = crate::next_streak_multiplier(4, 5, 6);
198 assert_eq!(streak, 5);
199 let r = hashrate_reward(1_000_000, streak, N, N, USD_UNIT).unwrap();
200 assert_eq!(r.total, 6);
201 }
202}