1use pot_o_core::TribeResult;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum PoolType {
13 Solo,
14 Proportional,
15 PPLNS,
16 Custom,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct MinerShare {
21 pub miner_pubkey: String,
22 pub share_pct: f64,
23 pub reward_amount: u64,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct MinerInfo {
28 pub pubkey: String,
29 pub stake: u64,
30 pub proofs_submitted: u64,
31 pub reputation: f64,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ProofRecord {
36 pub miner_pubkey: String,
37 pub challenge_id: String,
38 pub reward: u64,
39 pub timestamp: chrono::DateTime<chrono::Utc>,
40}
41
42#[derive(Debug, Clone, Default, Serialize, Deserialize)]
43pub struct PoolInfo {
44 pub pool_type: String,
45 pub total_miners: usize,
46 pub total_stake: u64,
47 pub minimum_stake: u64,
48}
49
50pub trait PoolStrategy: Send + Sync {
56 fn pool_type(&self) -> PoolType;
57 fn calculate_shares(&self, proofs: &[ProofRecord], reward: u64)
58 -> TribeResult<Vec<MinerShare>>;
59 fn minimum_stake(&self) -> u64;
60 fn accept_miner(&self, miner: &MinerInfo) -> TribeResult<bool>;
61 fn pool_info(&self, miners: usize, total_stake: u64) -> PoolInfo;
62}
63
64pub struct SoloStrategy;
69
70impl PoolStrategy for SoloStrategy {
71 fn pool_type(&self) -> PoolType {
72 PoolType::Solo
73 }
74
75 fn calculate_shares(
76 &self,
77 proofs: &[ProofRecord],
78 reward: u64,
79 ) -> TribeResult<Vec<MinerShare>> {
80 Ok(proofs
82 .last()
83 .map(|p| {
84 vec![MinerShare {
85 miner_pubkey: p.miner_pubkey.clone(),
86 share_pct: 100.0,
87 reward_amount: reward,
88 }]
89 })
90 .unwrap_or_default())
91 }
92
93 fn minimum_stake(&self) -> u64 {
94 0 }
96
97 fn accept_miner(&self, _miner: &MinerInfo) -> TribeResult<bool> {
98 Ok(true)
99 }
100
101 fn pool_info(&self, miners: usize, total_stake: u64) -> PoolInfo {
102 PoolInfo {
103 pool_type: "solo".into(),
104 total_miners: miners,
105 total_stake,
106 minimum_stake: 0,
107 }
108 }
109}
110
111fn distribute_rewards(
116 miner_counts: HashMap<String, u64>,
117 total_proofs: u64,
118 total_reward: u64,
119) -> Vec<MinerShare> {
120 let mut shares = Vec::new();
121
122 let mut calculated_rewards: Vec<(String, u64, f64)> = miner_counts
124 .into_iter()
125 .map(|(miner_pubkey, count)| {
126 let share_pct = (count as f64 / total_proofs as f64) * 100.0;
127 let reward_amount = (count as f64 / total_proofs as f64 * total_reward as f64) as u64;
128 (miner_pubkey, reward_amount, share_pct)
129 })
130 .collect();
131
132 let total_distributed: u64 = calculated_rewards.iter().map(|r| r.1).sum();
134 let remainder = total_reward - total_distributed;
135
136 if let Some(last) = calculated_rewards.last_mut() {
138 last.1 += remainder;
139 }
140
141 for (miner_pubkey, reward_amount, share_pct) in calculated_rewards {
143 shares.push(MinerShare {
144 miner_pubkey,
145 share_pct,
146 reward_amount,
147 });
148 }
149
150 shares
151}
152
153pub struct ProportionalPool {
158 pub min_stake: u64,
159}
160
161impl PoolStrategy for ProportionalPool {
162 fn pool_type(&self) -> PoolType {
163 PoolType::Proportional
164 }
165
166 fn calculate_shares(
167 &self,
168 proofs: &[ProofRecord],
169 reward: u64,
170 ) -> TribeResult<Vec<MinerShare>> {
171 if proofs.is_empty() {
172 return Ok(Vec::new());
173 }
174
175 let mut miner_counts: HashMap<String, u64> = HashMap::new();
177 for proof in proofs {
178 *miner_counts.entry(proof.miner_pubkey.clone()).or_insert(0) += 1;
179 }
180
181 let total_proofs = proofs.len() as u64;
182 let shares = distribute_rewards(miner_counts, total_proofs, reward);
183
184 Ok(shares)
185 }
186
187 fn minimum_stake(&self) -> u64 {
188 self.min_stake
189 }
190
191 fn accept_miner(&self, miner: &MinerInfo) -> TribeResult<bool> {
192 Ok(miner.stake >= self.min_stake)
193 }
194
195 fn pool_info(&self, miners: usize, total_stake: u64) -> PoolInfo {
196 PoolInfo {
197 pool_type: "proportional".into(),
198 total_miners: miners,
199 total_stake,
200 minimum_stake: self.min_stake,
201 }
202 }
203}
204
205pub struct PPLNSPool {
210 pub window_size: usize,
211 pub min_stake: u64,
212}
213
214impl PoolStrategy for PPLNSPool {
215 fn pool_type(&self) -> PoolType {
216 PoolType::PPLNS
217 }
218
219 fn calculate_shares(
220 &self,
221 proofs: &[ProofRecord],
222 reward: u64,
223 ) -> TribeResult<Vec<MinerShare>> {
224 if proofs.is_empty() {
225 return Ok(Vec::new());
226 }
227
228 let start_idx = if proofs.len() > self.window_size {
230 proofs.len() - self.window_size
231 } else {
232 0
233 };
234 let window_proofs = &proofs[start_idx..];
235
236 let mut miner_counts: HashMap<String, u64> = HashMap::new();
238 for proof in window_proofs {
239 *miner_counts.entry(proof.miner_pubkey.clone()).or_insert(0) += 1;
240 }
241
242 let total_proofs = window_proofs.len() as u64;
243 let shares = distribute_rewards(miner_counts, total_proofs, reward);
244
245 Ok(shares)
246 }
247
248 fn minimum_stake(&self) -> u64 {
249 self.min_stake
250 }
251
252 fn accept_miner(&self, miner: &MinerInfo) -> TribeResult<bool> {
253 Ok(miner.stake >= self.min_stake)
254 }
255
256 fn pool_info(&self, miners: usize, total_stake: u64) -> PoolInfo {
257 PoolInfo {
258 pool_type: "pplns".into(),
259 total_miners: miners,
260 total_stake,
261 minimum_stake: self.min_stake,
262 }
263 }
264}
265
266#[cfg(test)]
271mod tests {
272 use super::*;
273 use chrono::Utc;
274
275 fn create_proof(miner_pubkey: &str) -> ProofRecord {
276 ProofRecord {
277 miner_pubkey: miner_pubkey.to_string(),
278 challenge_id: "challenge1".to_string(),
279 reward: 0,
280 timestamp: Utc::now(),
281 }
282 }
283
284 fn create_miner(pubkey: &str, stake: u64) -> MinerInfo {
285 MinerInfo {
286 pubkey: pubkey.to_string(),
287 stake,
288 proofs_submitted: 0,
289 reputation: 1.0,
290 }
291 }
292
293 #[test]
298 fn test_proportional_single_miner() {
299 let strategy = ProportionalPool { min_stake: 0 };
300 let proofs = vec![
301 create_proof("miner_a"),
302 create_proof("miner_a"),
303 create_proof("miner_a"),
304 ];
305
306 let shares = strategy.calculate_shares(&proofs, 1000).unwrap();
307
308 assert_eq!(shares.len(), 1);
309 assert_eq!(shares[0].miner_pubkey, "miner_a");
310 assert_eq!(shares[0].share_pct, 100.0);
311 assert_eq!(shares[0].reward_amount, 1000);
312 }
313
314 #[test]
315 fn test_proportional_equal_shares() {
316 let strategy = ProportionalPool { min_stake: 0 };
317 let proofs = vec![
318 create_proof("miner_a"),
319 create_proof("miner_b"),
320 create_proof("miner_a"),
321 create_proof("miner_b"),
322 ];
323
324 let shares = strategy.calculate_shares(&proofs, 1000).unwrap();
325
326 assert_eq!(shares.len(), 2);
327
328 let miner_a = shares.iter().find(|s| s.miner_pubkey == "miner_a").unwrap();
330 assert_eq!(miner_a.share_pct, 50.0);
331 assert_eq!(miner_a.reward_amount, 500);
332
333 let miner_b = shares.iter().find(|s| s.miner_pubkey == "miner_b").unwrap();
335 assert_eq!(miner_b.share_pct, 50.0);
336 assert_eq!(miner_b.reward_amount, 500);
337 }
338
339 #[test]
340 fn test_proportional_unequal_shares() {
341 let strategy = ProportionalPool { min_stake: 0 };
342 let proofs = vec![
343 create_proof("miner_a"),
344 create_proof("miner_b"),
345 create_proof("miner_a"),
346 create_proof("miner_c"),
347 create_proof("miner_a"),
348 ];
349
350 let shares = strategy.calculate_shares(&proofs, 1000).unwrap();
351
352 assert_eq!(shares.len(), 3);
353
354 let miner_a = shares.iter().find(|s| s.miner_pubkey == "miner_a").unwrap();
356 assert_eq!(miner_a.share_pct, 60.0);
357 assert_eq!(miner_a.reward_amount, 600);
358
359 let miner_b = shares.iter().find(|s| s.miner_pubkey == "miner_b").unwrap();
361 assert_eq!(miner_b.share_pct, 20.0);
362 assert_eq!(miner_b.reward_amount, 200);
363
364 let miner_c = shares.iter().find(|s| s.miner_pubkey == "miner_c").unwrap();
366 assert_eq!(miner_c.share_pct, 20.0);
367 assert_eq!(miner_c.reward_amount, 200);
368 }
369
370 #[test]
371 fn test_proportional_empty_proofs() {
372 let strategy = ProportionalPool { min_stake: 0 };
373 let proofs = vec![];
374
375 let shares = strategy.calculate_shares(&proofs, 1000).unwrap();
376
377 assert_eq!(shares.len(), 0);
378 }
379
380 #[test]
381 fn test_proportional_fractional_rounding() {
382 let strategy = ProportionalPool { min_stake: 0 };
383 let proofs = vec![
384 create_proof("miner_a"),
385 create_proof("miner_b"),
386 create_proof("miner_c"),
387 ];
388
389 let shares = strategy.calculate_shares(&proofs, 1000).unwrap();
390
391 assert_eq!(shares.len(), 3);
392
393 let total: u64 = shares.iter().map(|s| s.reward_amount).sum();
395 assert_eq!(total, 1000); let pct_sum: f64 = shares.iter().map(|s| s.share_pct).sum();
399 assert!((pct_sum - 100.0).abs() < 0.01);
400 }
401
402 #[test]
407 fn test_pplns_window_excludes_old() {
408 let strategy = PPLNSPool {
409 window_size: 3,
410 min_stake: 0,
411 };
412 let proofs = vec![
413 create_proof("miner_old1"),
414 create_proof("miner_old2"),
415 create_proof("miner_a"),
416 create_proof("miner_b"),
417 create_proof("miner_a"),
418 ];
419
420 let shares = strategy.calculate_shares(&proofs, 1000).unwrap();
421
422 let has_old1 = shares.iter().any(|s| s.miner_pubkey == "miner_old1");
425 let has_old2 = shares.iter().any(|s| s.miner_pubkey == "miner_old2");
426
427 assert!(!has_old1);
428 assert!(!has_old2);
429
430 let total: u64 = shares.iter().map(|s| s.reward_amount).sum();
432 assert_eq!(total, 1000);
433
434 let miner_a = shares.iter().find(|s| s.miner_pubkey == "miner_a").unwrap();
436 assert!((miner_a.share_pct - 66.67).abs() < 0.1);
437 assert!(miner_a.reward_amount >= 666 && miner_a.reward_amount <= 667);
438
439 let miner_b = shares.iter().find(|s| s.miner_pubkey == "miner_b").unwrap();
441 assert!((miner_b.share_pct - 33.33).abs() < 0.1);
442 assert!(miner_b.reward_amount >= 333 && miner_b.reward_amount <= 334);
443 }
444
445 #[test]
446 fn test_pplns_single_miner_in_window() {
447 let strategy = PPLNSPool {
448 window_size: 5,
449 min_stake: 0,
450 };
451 let proofs = vec![
452 create_proof("miner_a"),
453 create_proof("miner_a"),
454 create_proof("miner_a"),
455 ];
456
457 let shares = strategy.calculate_shares(&proofs, 1000).unwrap();
458
459 assert_eq!(shares.len(), 1);
460 assert_eq!(shares[0].miner_pubkey, "miner_a");
461 assert_eq!(shares[0].share_pct, 100.0);
462 assert_eq!(shares[0].reward_amount, 1000);
463 }
464
465 #[test]
466 fn test_pplns_shared_window() {
467 let strategy = PPLNSPool {
468 window_size: 4,
469 min_stake: 0,
470 };
471 let proofs = vec![
472 create_proof("miner_a"),
473 create_proof("miner_a"),
474 create_proof("miner_b"),
475 create_proof("miner_c"),
476 create_proof("miner_b"),
477 ];
478
479 let shares = strategy.calculate_shares(&proofs, 1000).unwrap();
480
481 assert_eq!(shares.len(), 3);
487
488 let miner_a = shares.iter().find(|s| s.miner_pubkey == "miner_a").unwrap();
489 assert_eq!(miner_a.share_pct, 25.0);
490 assert_eq!(miner_a.reward_amount, 250);
491
492 let miner_b = shares.iter().find(|s| s.miner_pubkey == "miner_b").unwrap();
493 assert_eq!(miner_b.share_pct, 50.0);
494 assert_eq!(miner_b.reward_amount, 500);
495
496 let miner_c = shares.iter().find(|s| s.miner_pubkey == "miner_c").unwrap();
497 assert_eq!(miner_c.share_pct, 25.0);
498 assert_eq!(miner_c.reward_amount, 250);
499 }
500
501 #[test]
502 fn test_pplns_empty_proofs() {
503 let strategy = PPLNSPool {
504 window_size: 3,
505 min_stake: 0,
506 };
507 let proofs = vec![];
508
509 let shares = strategy.calculate_shares(&proofs, 1000).unwrap();
510
511 assert_eq!(shares.len(), 0);
512 }
513
514 #[test]
515 fn test_pplns_window_larger_than_proofs() {
516 let strategy = PPLNSPool {
517 window_size: 10,
518 min_stake: 0,
519 };
520 let proofs = vec![
521 create_proof("miner_a"),
522 create_proof("miner_b"),
523 create_proof("miner_a"),
524 ];
525
526 let shares = strategy.calculate_shares(&proofs, 1000).unwrap();
527
528 assert_eq!(shares.len(), 2);
533
534 let total: u64 = shares.iter().map(|s| s.reward_amount).sum();
536 assert_eq!(total, 1000);
537
538 let miner_a = shares.iter().find(|s| s.miner_pubkey == "miner_a").unwrap();
539 assert!((miner_a.share_pct - 66.67).abs() < 0.1);
540 assert!(miner_a.reward_amount >= 666 && miner_a.reward_amount <= 667);
541
542 let miner_b = shares.iter().find(|s| s.miner_pubkey == "miner_b").unwrap();
543 assert!((miner_b.share_pct - 33.33).abs() < 0.1);
544 assert!(miner_b.reward_amount >= 333 && miner_b.reward_amount <= 334);
545 }
546
547 #[test]
552 fn test_proportional_accept_miner_with_sufficient_stake() {
553 let strategy = ProportionalPool { min_stake: 100 };
554 let miner = create_miner("miner_a", 150);
555
556 let accepted = strategy.accept_miner(&miner).unwrap();
557
558 assert!(accepted);
559 }
560
561 #[test]
562 fn test_proportional_reject_miner_with_insufficient_stake() {
563 let strategy = ProportionalPool { min_stake: 100 };
564 let miner = create_miner("miner_a", 50);
565
566 let accepted = strategy.accept_miner(&miner).unwrap();
567
568 assert!(!accepted);
569 }
570
571 #[test]
572 fn test_pplns_accept_miner_with_sufficient_stake() {
573 let strategy = PPLNSPool {
574 window_size: 5,
575 min_stake: 200,
576 };
577 let miner = create_miner("miner_a", 300);
578
579 let accepted = strategy.accept_miner(&miner).unwrap();
580
581 assert!(accepted);
582 }
583
584 #[test]
585 fn test_pplns_reject_miner_with_insufficient_stake() {
586 let strategy = PPLNSPool {
587 window_size: 5,
588 min_stake: 200,
589 };
590 let miner = create_miner("miner_a", 100);
591
592 let accepted = strategy.accept_miner(&miner).unwrap();
593
594 assert!(!accepted);
595 }
596
597 #[test]
602 fn test_proportional_pool_info() {
603 let strategy = ProportionalPool { min_stake: 50 };
604
605 let info = strategy.pool_info(5, 500);
606
607 assert_eq!(info.pool_type, "proportional");
608 assert_eq!(info.total_miners, 5);
609 assert_eq!(info.total_stake, 500);
610 assert_eq!(info.minimum_stake, 50);
611 }
612
613 #[test]
614 fn test_pplns_pool_info() {
615 let strategy = PPLNSPool {
616 window_size: 10,
617 min_stake: 100,
618 };
619
620 let info = strategy.pool_info(3, 300);
621
622 assert_eq!(info.pool_type, "pplns");
623 assert_eq!(info.total_miners, 3);
624 assert_eq!(info.total_stake, 300);
625 assert_eq!(info.minimum_stake, 100);
626 }
627
628 #[test]
633 fn test_solo_pool_type() {
634 let strategy = SoloStrategy;
635 assert_eq!(strategy.pool_type(), PoolType::Solo);
636 }
637
638 #[test]
639 fn test_solo_minimum_stake() {
640 let strategy = SoloStrategy;
641 assert_eq!(strategy.minimum_stake(), 0);
642 }
643
644 #[test]
645 fn test_solo_accept_miner() {
646 let strategy = SoloStrategy;
647 let miner = create_miner("miner_a", 0);
648 assert!(strategy.accept_miner(&miner).unwrap());
649 }
650
651 #[test]
652 fn test_solo_calculate_shares() {
653 let strategy = SoloStrategy;
654 let proofs = vec![
655 create_proof("miner_a"),
656 create_proof("miner_b"),
657 create_proof("miner_c"),
658 ];
659
660 let shares = strategy.calculate_shares(&proofs, 1000).unwrap();
661
662 assert_eq!(shares.len(), 1);
664 assert_eq!(shares[0].miner_pubkey, "miner_c");
665 assert_eq!(shares[0].reward_amount, 1000);
666 }
667
668 #[test]
669 fn test_solo_empty_proofs() {
670 let strategy = SoloStrategy;
671 let proofs = vec![];
672
673 let shares = strategy.calculate_shares(&proofs, 1000).unwrap();
674
675 assert_eq!(shares.len(), 0);
676 }
677}