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 const STRIKE_BOOST_ROUNDS: u32 = 240;
20
21pub const STRIKE_BOOST_HASHRATE_MULTIPLIER: u64 = 2;
26
27pub fn get_hashrate_ui_amount(hashrate: u64) -> String {
31 format!("{}.{:02}", hashrate / HASHRATE_PER_TICKET, hashrate % HASHRATE_PER_TICKET)
32}
33
34pub fn get_hashrate_tickets_count(hashrate: u64) -> u64 {
37 hashrate / HASHRATE_PER_TICKET
38}
39
40#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
44pub struct HashrateReward {
45 pub loyalty_points: u64,
47 pub skill_points: u64,
49 pub total: u64,
51}
52
53#[derive(Debug, thiserror::Error)]
54pub enum HashrateRewardError {
55 #[error("hashrate reward overflows u64")]
56 MathOverflow,
57}
58
59pub fn hashrate_reward(
80 stake: u64,
81 streak: u32,
82 covered: u32,
83 total_tiles: u32,
84 usd_unit: u64,
85 hashrate_multiplier: u64,
86) -> Result<HashrateReward, HashrateRewardError> {
87 if stake == 0 || covered == 0 {
89 return Ok(HashrateReward::default());
90 }
91
92 let covered = covered as u128;
93 let denom = covered * usd_unit as u128;
94 let base = stake as u128;
95
96 let skill_num = base * (SKILL_WEIGHT as u128 * total_tiles as u128);
99 let loyalty_num = base * (LOYALTY_WEIGHT as u128 * streak as u128 * covered);
100
101 let total: u64 = ((loyalty_num + skill_num) / denom)
102 .try_into()
103 .map_err(|_| HashrateRewardError::MathOverflow)?;
104 let total = total
107 .checked_mul(hashrate_multiplier)
108 .ok_or(HashrateRewardError::MathOverflow)?;
109 let skill_points: u64 = (skill_num / denom) as u64 * hashrate_multiplier;
112 let loyalty_points = total - skill_points;
113
114 Ok(HashrateReward {
115 loyalty_points,
116 skill_points,
117 total,
118 })
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 const USD_UNIT: u64 = 1_000_000; const N: u32 = 21;
127
128 const NO_BOOST: u64 = 1;
130
131 #[test]
132 fn boost_doubles_total_and_channels_still_sum() {
133 let r = hashrate_reward(2_000_000, 15, 3, N, USD_UNIT, STRIKE_BOOST_HASHRATE_MULTIPLIER).unwrap();
135 assert_eq!(r.total, 88);
136 assert_eq!(r.loyalty_points + r.skill_points, r.total);
137 }
138
139 #[test]
140 fn boost_matches_program_boosted_settle() {
141 let r = hashrate_reward(1_000_000, 2, N, N, USD_UNIT, STRIKE_BOOST_HASHRATE_MULTIPLIER).unwrap();
144 assert_eq!(r.total, 6);
145 }
146
147 #[test]
148 fn worked_example_two_dollars_streak_15_three_tiles() {
149 let r = hashrate_reward(2_000_000, 15, 3, N, USD_UNIT, NO_BOOST).unwrap();
151 assert_eq!(r.total, 44);
152 }
153
154 #[test]
155 fn channels_sum_to_total() {
156 let r = hashrate_reward(2_000_000, 15, 3, N, USD_UNIT, NO_BOOST).unwrap();
157 assert_eq!(r.loyalty_points + r.skill_points, r.total);
158 }
159
160 #[test]
161 fn full_coverage_has_no_skill_bonus_beyond_floor() {
162 let r = hashrate_reward(1_000_000, 1, N, N, USD_UNIT, NO_BOOST).unwrap();
165 assert_eq!(r.total, 2);
166 assert_eq!(r.skill_points, 1);
167 assert_eq!(r.loyalty_points, 1);
168 }
169
170 #[test]
171 fn single_tile_is_max_conviction() {
172 let r = hashrate_reward(1_000_000, 1, 1, N, USD_UNIT, NO_BOOST).unwrap();
175 assert_eq!(r.total, 22);
176 }
177
178 #[test]
179 fn ceiling_multiplier_per_dollar() {
180 let r = hashrate_reward(1_000_000, 30, 1, N, USD_UNIT, NO_BOOST).unwrap();
182 assert_eq!(r.total, 51);
183 }
184
185 #[test]
186 fn one_dollar_at_max_streak_earns_1_01_points() {
187 let r = hashrate_reward(1_000_000, 100, N, N, USD_UNIT, NO_BOOST).unwrap();
190 assert_eq!(r.total, 101);
191 assert_eq!(HASHRATE_PER_TICKET, 10u64.pow(HASHRATE_DECIMALS as u32));
192 }
193
194 #[test]
195 fn zero_stake_earns_nothing() {
196 let r = hashrate_reward(0, 30, 1, N, USD_UNIT, NO_BOOST).unwrap();
197 assert_eq!(r, HashrateReward::default());
198 }
199
200 #[test]
201 fn zero_coverage_earns_nothing_without_dividing_by_zero() {
202 let r = hashrate_reward(1_000_000, 5, 0, N, USD_UNIT, NO_BOOST).unwrap();
203 assert_eq!(r, HashrateReward::default());
204 }
205
206 #[test]
207 fn sub_unit_stake_floors_to_zero() {
208 let r = hashrate_reward(10_000, 1, N, N, USD_UNIT, NO_BOOST).unwrap();
210 assert_eq!(r.total, 0);
211 }
212
213 #[test]
214 fn ui_amount_formats_raw_units_with_two_decimals() {
215 assert_eq!(get_hashrate_ui_amount(101), "1.01");
216 assert_eq!(get_hashrate_ui_amount(0), "0.00");
217 assert_eq!(get_hashrate_ui_amount(5), "0.05");
218 assert_eq!(get_hashrate_ui_amount(230), "2.30");
219 assert_eq!(get_hashrate_ui_amount(u64::MAX), "184467440737095516.15");
220 }
221
222 #[test]
223 fn tickets_count_floors_raw_units_to_whole_tickets() {
224 assert_eq!(get_hashrate_tickets_count(0), 0);
225 assert_eq!(get_hashrate_tickets_count(99), 0);
226 assert_eq!(get_hashrate_tickets_count(100), 1);
227 assert_eq!(get_hashrate_tickets_count(6_550), 65);
228 assert_eq!(get_hashrate_tickets_count(u64::MAX), u64::MAX / 100);
229 }
230
231 #[test]
232 fn matches_next_streak_multiplier_for_an_upcoming_deploy() {
233 let streak = crate::next_streak_multiplier(4, 5, 6);
236 assert_eq!(streak, 5);
237 let r = hashrate_reward(1_000_000, streak, N, N, USD_UNIT, NO_BOOST).unwrap();
238 assert_eq!(r.total, 6);
239 }
240}