1use serde::{Deserialize, Serialize};
2use solana_program::keccak;
3use steel::*;
4
5use crate::state::{round_pda, OreAccount};
6
7#[repr(C)]
8#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Serialize, Deserialize)]
9pub struct Round {
10 pub id: u64,
12
13 pub deployed: [u64; 25],
16
17 pub mass: [u64; 25],
19
20 pub count: [u64; 25],
23
24 pub slot_hash: [u8; 32],
27
28 pub expires_at: u64,
31
32 pub motherlode: u64,
34
35 pub rent_payer: Pubkey,
37
38 pub rewards: [u64; 25],
40
41 pub total_vaulted: u64,
44
45 pub total_returned_sol: u64,
48
49 pub total_miners: u64,
52
53 pub top_miner: Pubkey,
56}
57
58impl Round {
59 pub fn pda(&self) -> (Pubkey, u8) {
60 round_pda(self.id)
61 }
62
63 pub fn rng(&self) -> Option<u64> {
64 if self.slot_hash == [0; 32] || self.slot_hash == [u8::MAX; 32] {
65 return None;
66 }
67 let r1 = u64::from_le_bytes(self.slot_hash[0..8].try_into().unwrap());
68 let r2 = u64::from_le_bytes(self.slot_hash[8..16].try_into().unwrap());
69 let r3 = u64::from_le_bytes(self.slot_hash[16..24].try_into().unwrap());
70 let r4 = u64::from_le_bytes(self.slot_hash[24..32].try_into().unwrap());
71 let r = r1 ^ r2 ^ r3 ^ r4;
72 Some(r)
73 }
74
75 pub fn winning_square(&self, rng: u64) -> usize {
76 (rng % 25) as usize
77 }
78
79 pub fn top_miner_sample(&self, rng: u64, winning_square: usize) -> u64 {
80 if self.deployed[winning_square] == 0 {
81 return 0;
82 }
83 rng.reverse_bits() % self.deployed[winning_square]
84 }
85
86 pub fn calculate_fees(&self, winning_square: usize) -> (u64, u64) {
87 let mut admin_fee = 0;
88 let mut protocol_fee = 0;
89 for (i, &deployed) in self.deployed.iter().enumerate() {
90 if deployed > 0 {
91 let sq_admin = ((deployed / 100) as u64).max(1);
92 admin_fee += sq_admin;
93 if i != winning_square {
94 protocol_fee += ((deployed.saturating_sub(sq_admin) / 10) as u64).max(1);
95 }
96 }
97 }
98 (admin_fee, protocol_fee)
99 }
100
101 pub fn is_split_reward(&self, winning_square: usize) -> bool {
104 self.distribution_mask() & (1 << winning_square) == 0
105 }
106
107 pub fn did_hit_motherlode(&self, rng: u64) -> bool {
108 rng.reverse_bits() % 500 == 0
109 }
110
111 pub fn total_deployed(&self) -> u64 {
112 self.deployed.iter().sum()
113 }
114
115 pub fn top_miner_reward(&self) -> u64 {
116 self.rewards.iter().sum()
117 }
118
119 pub fn distribution_mask(&self) -> u32 {
126 const BITS: u32 = 10;
127 let rng = keccak::hashv(&[self.id.to_le_bytes().as_ref()]);
128
129 let mut indices: [u8; 25] = [0; 25];
132 for i in 0..25 {
133 indices[i] = i as u8;
134 }
135
136 let mut randomness = rng.0;
138 let mut random_offset = 0;
139
140 for i in (1..25).rev() {
142 if random_offset + 2 > randomness.len() {
144 randomness = keccak::hashv(&[&randomness]).0;
146 random_offset = 0;
147 }
148 let mut two_bytes = [0u8; 2];
149 two_bytes.copy_from_slice(&randomness[random_offset..random_offset + 2]);
150 let r = u16::from_le_bytes(two_bytes);
151 let j = (r as usize) % (i + 1);
152 indices.swap(i, j);
153 random_offset += 2;
154 }
155
156 let mut mask: u32 = 0;
158 for &idx in &indices[..BITS as usize] {
159 mask |= 1 << idx;
160 }
161
162 mask
164 }
165}
166
167account!(OreAccount, Round);
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 fn default_round(id: u64) -> Round {
174 Round {
175 id,
176 deployed: [0; 25],
177 mass: [0; 25],
178 count: [0; 25],
179 slot_hash: [0; 32],
180 expires_at: 0,
181 motherlode: 0,
182 rent_payer: Pubkey::default(),
183 rewards: [0; 25],
184 total_vaulted: 0,
185 total_returned_sol: 0,
186 total_miners: 0,
187 top_miner: Pubkey::default(),
188 }
189 }
190
191 #[test]
192 fn test_distribution_mask_has_exactly_10_bits_set() {
193 for id in 0..1000 {
194 let round = default_round(id);
195 let mask = round.distribution_mask();
196 assert_eq!(
197 mask.count_ones(),
198 10,
199 "Round {id}: expected 10 bits set, got {}",
200 mask.count_ones()
201 );
202 }
203 }
204
205 #[test]
206 fn test_distribution_mask_only_uses_first_25_bits() {
207 for id in 0..1000 {
208 let round = default_round(id);
209 let mask = round.distribution_mask();
210 assert_eq!(
211 mask & !((1u32 << 25) - 1),
212 0,
213 "Round {id}: bits above position 24 should not be set"
214 );
215 }
216 }
217
218 #[test]
219 fn test_distribution_mask_is_deterministic() {
220 for id in 0..100 {
221 let round = default_round(id);
222 let mask1 = round.distribution_mask();
223 let mask2 = round.distribution_mask();
224 assert_eq!(mask1, mask2, "Round {id}: mask should be deterministic");
225 }
226 }
227
228 #[test]
229 fn test_distribution_mask_values_are_randomized() {
230 let mut masks = std::collections::HashSet::new();
231 for id in 0..100 {
232 let round = default_round(id);
233 masks.insert(round.distribution_mask());
234 }
235 assert!(
236 masks.len() > 50,
237 "Expected diverse mask values across rounds, only got {} unique values out of 100",
238 masks.len()
239 );
240 }
241}