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_winnings: 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_total_winnings(&self, winning_square: usize) -> u64 {
87 let mut total_winnings = 0;
88 for (i, &deployed) in self.deployed.iter().enumerate() {
89 if i != winning_square {
90 total_winnings += deployed;
91 }
92 }
93 total_winnings
94 }
95
96 pub fn is_split_reward(&self, rng: u64) -> bool {
97 let rng = rng.reverse_bits().to_le_bytes();
99 let r1 = u16::from_le_bytes(rng[0..2].try_into().unwrap());
100 let r2 = u16::from_le_bytes(rng[2..4].try_into().unwrap());
101 let r3 = u16::from_le_bytes(rng[4..6].try_into().unwrap());
102 let r4 = u16::from_le_bytes(rng[6..8].try_into().unwrap());
103 let r = r1 ^ r2 ^ r3 ^ r4;
104 r % 2 == 0
105 }
106
107 pub fn is_split_reward_v2(&self, winning_square: usize) -> bool {
110 self.distribution_mask() & (1 << winning_square) == 0
111 }
112
113 pub fn did_hit_motherlode(&self, rng: u64) -> bool {
114 rng.reverse_bits() % 500 == 0
115 }
116
117 pub fn total_deployed(&self) -> u64 {
118 self.deployed.iter().sum()
119 }
120
121 pub fn top_miner_reward(&self) -> u64 {
122 self.rewards.iter().sum()
123 }
124
125 pub fn distribution_mask(&self) -> u32 {
132 const BITS: u32 = 10;
133 let rng = keccak::hashv(&[self.id.to_le_bytes().as_ref()]);
134
135 let mut indices: [u8; 25] = [0; 25];
138 for i in 0..25 {
139 indices[i] = i as u8;
140 }
141
142 let mut randomness = rng.0;
144 let mut random_offset = 0;
145
146 for i in (1..25).rev() {
148 if random_offset + 2 > randomness.len() {
150 randomness = keccak::hashv(&[&randomness]).0;
152 random_offset = 0;
153 }
154 let mut two_bytes = [0u8; 2];
155 two_bytes.copy_from_slice(&randomness[random_offset..random_offset + 2]);
156 let r = u16::from_le_bytes(two_bytes);
157 let j = (r as usize) % (i + 1);
158 indices.swap(i, j);
159 random_offset += 2;
160 }
161
162 let mut mask: u32 = 0;
164 for &idx in &indices[..BITS as usize] {
165 mask |= 1 << idx;
166 }
167
168 mask
170 }
171}
172
173account!(OreAccount, Round);
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn default_round(id: u64) -> Round {
180 Round {
181 id,
182 deployed: [0; 25],
183 mass: [0; 25],
184 count: [0; 25],
185 slot_hash: [0; 32],
186 expires_at: 0,
187 motherlode: 0,
188 rent_payer: Pubkey::default(),
189 rewards: [0; 25],
190 total_vaulted: 0,
191 total_winnings: 0,
192 total_miners: 0,
193 top_miner: Pubkey::default(),
194 }
195 }
196
197 #[test]
198 fn test_distribution_mask_has_exactly_10_bits_set() {
199 for id in 0..1000 {
200 let round = default_round(id);
201 let mask = round.distribution_mask();
202 assert_eq!(
203 mask.count_ones(),
204 10,
205 "Round {id}: expected 10 bits set, got {}",
206 mask.count_ones()
207 );
208 }
209 }
210
211 #[test]
212 fn test_distribution_mask_only_uses_first_25_bits() {
213 for id in 0..1000 {
214 let round = default_round(id);
215 let mask = round.distribution_mask();
216 assert_eq!(
217 mask & !((1u32 << 25) - 1),
218 0,
219 "Round {id}: bits above position 24 should not be set"
220 );
221 }
222 }
223
224 #[test]
225 fn test_distribution_mask_is_deterministic() {
226 for id in 0..100 {
227 let round = default_round(id);
228 let mask1 = round.distribution_mask();
229 let mask2 = round.distribution_mask();
230 assert_eq!(mask1, mask2, "Round {id}: mask should be deterministic");
231 }
232 }
233
234 #[test]
235 fn test_distribution_mask_values_are_randomized() {
236 let mut masks = std::collections::HashSet::new();
237 for id in 0..100 {
238 let round = default_round(id);
239 masks.insert(round.distribution_mask());
240 }
241 assert!(
242 masks.len() > 50,
243 "Expected diverse mask values across rounds, only got {} unique values out of 100",
244 masks.len()
245 );
246 }
247}