1use std::collections::{HashMap, HashSet};
14use std::time::{Duration, Instant};
16
17use async_trait::async_trait;
18
19use crate::adaptive::{NodeId, performance::PerformanceMonitor, trust::EigenTrustEngine};
20use crate::placement::{
21 GeographicLocation, NetworkRegion, PlacementConfig, PlacementDecision, PlacementError,
22 PlacementResult, PlacementStrategy,
23};
24#[derive(Debug, Clone)]
28pub struct WeightedSampler {
29 #[allow(dead_code)]
31 rng_state: u64,
32 #[allow(dead_code)]
34 weight_cache: HashMap<NodeId, f64>,
35 #[allow(dead_code)]
37 cache_updated: Instant,
38 #[allow(dead_code)]
40 cache_ttl: Duration,
41}
42
43impl Default for WeightedSampler {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl WeightedSampler {
50 pub fn new() -> Self {
52 Self {
53 rng_state: fastrand::u64(..),
54 weight_cache: HashMap::new(),
55 cache_updated: Instant::now(),
56 cache_ttl: Duration::from_secs(300), }
58 }
59
60 pub fn calculate_weight(
68 &self,
69 node_id: &NodeId,
70 trust_score: f64,
71 stability_score: f64,
72 capacity_factor: f64,
73 diversity_factor: f64,
74 alpha: f64,
75 beta: f64,
76 gamma: f64,
77 ) -> PlacementResult<f64> {
78 if !(0.0..=1.0).contains(&trust_score) {
80 return Err(PlacementError::InvalidWeight {
81 node_id: node_id.clone(),
82 weight: trust_score,
83 reason: "Trust score must be between 0.0 and 1.0".to_string(),
84 });
85 }
86
87 if !(0.0..=1.0).contains(&stability_score) {
88 return Err(PlacementError::InvalidWeight {
89 node_id: node_id.clone(),
90 weight: stability_score,
91 reason: "Stability score must be between 0.0 and 1.0".to_string(),
92 });
93 }
94
95 if capacity_factor < 0.0 {
96 return Err(PlacementError::InvalidWeight {
97 node_id: node_id.clone(),
98 weight: capacity_factor,
99 reason: "Capacity factor must be non-negative".to_string(),
100 });
101 }
102
103 if diversity_factor < 0.0 {
104 return Err(PlacementError::InvalidWeight {
105 node_id: node_id.clone(),
106 weight: diversity_factor,
107 reason: "Diversity factor must be non-negative".to_string(),
108 });
109 }
110
111 let trust_component = if alpha == 0.0 {
113 1.0
114 } else {
115 trust_score.powf(alpha)
116 };
117 let stability_component = if beta == 0.0 {
118 1.0
119 } else {
120 stability_score.powf(beta)
121 };
122 let capacity_component = if gamma == 0.0 {
123 1.0
124 } else {
125 capacity_factor.powf(gamma)
126 };
127
128 let weight = trust_component * stability_component * capacity_component * diversity_factor;
129
130 if !weight.is_finite() || weight <= 0.0 {
132 return Err(PlacementError::InvalidWeight {
133 node_id: node_id.clone(),
134 weight,
135 reason: "Computed weight is not finite or positive".to_string(),
136 });
137 }
138
139 Ok(weight)
140 }
141
142 pub fn sample_nodes(
144 &mut self,
145 candidates: &[(NodeId, f64)],
146 k: usize,
147 ) -> PlacementResult<Vec<NodeId>> {
148 if candidates.is_empty() {
149 return Err(PlacementError::InsufficientNodes {
150 required: k,
151 available: 0,
152 });
153 }
154
155 if k > candidates.len() {
156 return Err(PlacementError::InsufficientNodes {
157 required: k,
158 available: candidates.len(),
159 });
160 }
161
162 if k == 0 {
163 return Ok(Vec::new());
164 }
165
166 let mut weighted_keys: Vec<(f64, NodeId)> = candidates
168 .iter()
169 .map(|(node_id, weight)| {
170 if *weight <= 0.0 {
171 return Err(PlacementError::InvalidWeight {
172 node_id: node_id.clone(),
173 weight: *weight,
174 reason: "Weight must be positive".to_string(),
175 });
176 }
177
178 let u = fastrand::f64();
180
181 let key = u.powf(1.0 / weight);
183
184 Ok((key, node_id.clone()))
185 })
186 .collect::<PlacementResult<Vec<_>>>()?;
187
188 weighted_keys.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
190
191 Ok(weighted_keys
192 .into_iter()
193 .take(k)
194 .map(|(_, node_id)| node_id)
195 .collect())
196 }
197}
198
199#[derive(Debug, Clone)]
201pub struct DiversityEnforcer {
202 min_geographic_distance: f64,
204 max_nodes_per_region: usize,
206 max_nodes_per_asn: usize,
208 diversity_penalty: f64,
210}
211
212impl Default for DiversityEnforcer {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218impl DiversityEnforcer {
219 pub fn new() -> Self {
221 Self {
222 min_geographic_distance: 100.0, max_nodes_per_region: 2,
224 max_nodes_per_asn: 3,
225 diversity_penalty: 0.5, }
227 }
228
229 pub fn calculate_diversity_factor(
231 &self,
232 _candidate: &NodeId,
233 candidate_location: &GeographicLocation,
234 candidate_asn: u32,
235 candidate_region: &NetworkRegion,
236 selected_nodes: &[(NodeId, GeographicLocation, u32, NetworkRegion)],
237 ) -> f64 {
238 let mut diversity_factor = 1.0;
239
240 for (_, location, _, _) in selected_nodes {
242 let distance = candidate_location.distance_km(location);
243 if distance < self.min_geographic_distance {
244 diversity_factor *= self.diversity_penalty;
245 }
246 }
247
248 let region_count = selected_nodes
250 .iter()
251 .filter(|(_, _, _, region)| region == candidate_region)
252 .count();
253
254 if region_count >= self.max_nodes_per_region {
255 diversity_factor *= self.diversity_penalty;
256 }
257
258 let asn_count = selected_nodes
260 .iter()
261 .filter(|(_, _, asn, _)| *asn == candidate_asn)
262 .count();
263
264 if asn_count >= self.max_nodes_per_asn {
265 diversity_factor *= self.diversity_penalty;
266 }
267
268 diversity_factor.max(0.1) }
270
271 pub fn validate_selection(
273 &self,
274 selection: &[(NodeId, GeographicLocation, u32, NetworkRegion)],
275 ) -> PlacementResult<()> {
276 for (i, (node_a, loc_a, _, _)) in selection.iter().enumerate() {
278 for (j, (node_b, loc_b, _, _)) in selection.iter().enumerate() {
279 if i != j {
280 let distance = loc_a.distance_km(loc_b);
281 if distance < self.min_geographic_distance / 2.0 {
282 return Err(PlacementError::DiversityViolation {
283 constraint: "geographic_distance".to_string(),
284 nodes: vec![node_a.clone(), node_b.clone()],
285 details: format!(
286 "Distance {} km < minimum {} km",
287 distance,
288 self.min_geographic_distance / 2.0
289 ),
290 });
291 }
292 }
293 }
294 }
295
296 let mut region_counts: HashMap<NetworkRegion, usize> = HashMap::new();
298 for (_, _, _, region) in selection {
299 *region_counts.entry(*region).or_insert(0) += 1;
300 }
301
302 for (region, count) in region_counts {
303 if count > self.max_nodes_per_region {
304 return Err(PlacementError::DiversityViolation {
305 constraint: "region_distribution".to_string(),
306 nodes: selection
307 .iter()
308 .filter(|(_, _, _, r)| *r == region)
309 .map(|(node_id, _, _, _)| node_id.clone())
310 .collect(),
311 details: format!(
312 "Region {:?} has {} nodes > maximum {}",
313 region, count, self.max_nodes_per_region
314 ),
315 });
316 }
317 }
318
319 let mut asn_counts: HashMap<u32, usize> = HashMap::new();
321 for (_, _, asn, _) in selection {
322 *asn_counts.entry(*asn).or_insert(0) += 1;
323 }
324
325 for (asn, count) in asn_counts {
326 if count > self.max_nodes_per_asn {
327 return Err(PlacementError::DiversityViolation {
328 constraint: "asn_distribution".to_string(),
329 nodes: selection
330 .iter()
331 .filter(|(_, _, a, _)| *a == asn)
332 .map(|(node_id, _, _, _)| node_id.clone())
333 .collect(),
334 details: format!(
335 "ASN {} has {} nodes > maximum {}",
336 asn, count, self.max_nodes_per_asn
337 ),
338 });
339 }
340 }
341
342 Ok(())
343 }
344}
345
346#[derive(Debug)]
348pub struct WeightedPlacementStrategy {
349 sampler: WeightedSampler,
350 diversity_enforcer: DiversityEnforcer,
351 config: PlacementConfig,
352}
353
354impl WeightedPlacementStrategy {
355 pub fn new(config: PlacementConfig) -> Self {
357 Self {
358 sampler: WeightedSampler::new(),
359 diversity_enforcer: DiversityEnforcer::new(),
360 config,
361 }
362 }
363
364 async fn calculate_weights(
366 &self,
367 candidates: &HashSet<NodeId>,
368 _trust_system: &EigenTrustEngine,
369 _performance_monitor: &PerformanceMonitor,
370 node_metadata: &HashMap<NodeId, (GeographicLocation, u32, NetworkRegion)>,
371 selected_nodes: &[(NodeId, GeographicLocation, u32, NetworkRegion)],
372 ) -> PlacementResult<Vec<(NodeId, f64)>> {
373 let mut weights = Vec::new();
374
375 for node_id in candidates {
376 let trust_score = 0.8; let stability_score = 0.9; let capacity_factor = 1.0; let (location, asn, region) = node_metadata
387 .get(node_id)
388 .ok_or_else(|| PlacementError::NodeMetadataNotFound(node_id.clone()))?;
389
390 let diversity_factor = self.diversity_enforcer.calculate_diversity_factor(
392 node_id,
393 location,
394 *asn,
395 region,
396 selected_nodes,
397 );
398
399 let weight = self.sampler.calculate_weight(
401 node_id,
402 trust_score,
403 stability_score,
404 capacity_factor,
405 diversity_factor,
406 self.config.optimization_weights.trust_weight,
407 self.config.optimization_weights.performance_weight,
408 self.config.optimization_weights.capacity_weight,
409 )?;
410
411 weights.push((node_id.clone(), weight));
412 }
413
414 weights.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
416
417 Ok(weights)
418 }
419}
420
421#[async_trait]
422impl PlacementStrategy for WeightedPlacementStrategy {
423 async fn select_nodes(
424 &mut self,
425 candidates: &HashSet<NodeId>,
426 replication_factor: u8,
427 trust_system: &EigenTrustEngine,
428 performance_monitor: &PerformanceMonitor,
429 node_metadata: &HashMap<NodeId, (GeographicLocation, u32, NetworkRegion)>,
430 ) -> PlacementResult<PlacementDecision> {
431 let start_time = Instant::now();
432 let k = replication_factor as usize;
433
434 if candidates.is_empty() {
435 return Err(PlacementError::InsufficientNodes {
436 required: k,
437 available: 0,
438 });
439 }
440
441 if k > candidates.len() {
442 return Err(PlacementError::InsufficientNodes {
443 required: k,
444 available: candidates.len(),
445 });
446 }
447
448 let mut selected_nodes = Vec::new();
449 let mut remaining_candidates = candidates.clone();
450
451 for round in 0..k {
453 if remaining_candidates.is_empty() {
454 return Err(PlacementError::InsufficientNodes {
455 required: k - round,
456 available: 0,
457 });
458 }
459
460 let weights = self
462 .calculate_weights(
463 &remaining_candidates,
464 trust_system,
465 performance_monitor,
466 node_metadata,
467 &selected_nodes,
468 )
469 .await?;
470
471 let selected = self.sampler.sample_nodes(&weights, 1)?;
473 let selected_node = selected[0].clone();
474
475 let (location, asn, region) = node_metadata
477 .get(&selected_node)
478 .ok_or_else(|| PlacementError::NodeMetadataNotFound(selected_node.clone()))?;
479
480 selected_nodes.push((selected_node.clone(), *location, *asn, *region));
481
482 remaining_candidates.remove(&selected_node);
484 }
485
486 self.diversity_enforcer
488 .validate_selection(&selected_nodes)?;
489
490 let selection_time = start_time.elapsed();
491
492 let decision = PlacementDecision {
494 selected_nodes: selected_nodes
495 .into_iter()
496 .map(|(node_id, _, _, _)| node_id)
497 .collect(),
498 backup_nodes: Vec::new(), placement_strategy: "weighted_efraimidis_spirakis".to_string(),
500 diversity_score: 1.0, estimated_reliability: 0.95, selection_time,
503 metadata: HashMap::new(),
504 };
505
506 Ok(decision)
507 }
508
509 fn name(&self) -> &str {
510 "WeightedPlacementStrategy"
511 }
512
513 fn supports_constraints(&self) -> bool {
514 true
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 fn create_test_location(lat: f64, lon: f64) -> GeographicLocation {
522 GeographicLocation::new(lat, lon).unwrap()
523 }
524
525 #[test]
526 fn test_weight_calculation() {
527 let sampler = WeightedSampler::new();
528 let node_id = NodeId::from_bytes([1u8; 32]);
529
530 let weight = sampler
532 .calculate_weight(
533 &node_id, 0.8, 0.9, 1.2, 1.0, 1.0, 1.0, 1.0, )
541 .unwrap();
542
543 assert!((weight - (0.8 * 0.9 * 1.2 * 1.0)).abs() < 1e-10);
544
545 assert!(
547 sampler
548 .calculate_weight(&node_id, -0.1, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0)
549 .is_err()
550 );
551 assert!(
552 sampler
553 .calculate_weight(&node_id, 1.1, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0)
554 .is_err()
555 );
556 assert!(
557 sampler
558 .calculate_weight(&node_id, 0.5, -0.1, 1.0, 1.0, 1.0, 1.0, 1.0)
559 .is_err()
560 );
561 assert!(
562 sampler
563 .calculate_weight(&node_id, 0.5, 0.5, -1.0, 1.0, 1.0, 1.0, 1.0)
564 .is_err()
565 );
566 }
567
568 #[test]
569 fn test_efraimidis_spirakis_sampling() {
570 let mut sampler = WeightedSampler::new();
571
572 let candidates = vec![
573 (NodeId::from_bytes([1u8; 32]), 0.8),
574 (NodeId::from_bytes([2u8; 32]), 0.6),
575 (NodeId::from_bytes([3u8; 32]), 0.4),
576 (NodeId::from_bytes([4u8; 32]), 0.2),
577 ];
578
579 let selected = sampler.sample_nodes(&candidates, 2).unwrap();
581 assert_eq!(selected.len(), 2);
582 assert_ne!(selected[0], selected[1]);
583
584 assert!(sampler.sample_nodes(&candidates, 0).unwrap().is_empty());
586 assert!(sampler.sample_nodes(&candidates, 5).is_err());
587 assert!(sampler.sample_nodes(&[], 1).is_err());
588
589 let bad_candidates = vec![
591 (NodeId::from_bytes([1u8; 32]), 0.0),
592 (NodeId::from_bytes([2u8; 32]), 0.5),
593 ];
594 assert!(sampler.sample_nodes(&bad_candidates, 1).is_err());
595 }
596
597 #[test]
598 fn test_diversity_factor_calculation() {
599 let enforcer = DiversityEnforcer::new();
600 let candidate_id = NodeId::from_bytes([1u8; 32]);
601 let candidate_location = create_test_location(40.7128, -74.0060); let candidate_asn = 12345;
603 let candidate_region = NetworkRegion::NorthAmerica;
604
605 let factor = enforcer.calculate_diversity_factor(
607 &candidate_id,
608 &candidate_location,
609 candidate_asn,
610 &candidate_region,
611 &[],
612 );
613 assert_eq!(factor, 1.0);
614
615 let nearby_location = create_test_location(40.7589, -73.9851); let existing = vec![(
618 NodeId::from_bytes([2u8; 32]),
619 nearby_location,
620 54321,
621 NetworkRegion::NorthAmerica,
622 )];
623
624 let factor = enforcer.calculate_diversity_factor(
625 &candidate_id,
626 &candidate_location,
627 candidate_asn,
628 &candidate_region,
629 &existing,
630 );
631 assert!(factor < 1.0); let far_location = create_test_location(34.0522, -118.2437); let existing_same_asn = vec![(
636 NodeId::from_bytes([3u8; 32]),
637 far_location,
638 candidate_asn, NetworkRegion::NorthAmerica,
640 )];
641
642 let factor = enforcer.calculate_diversity_factor(
643 &candidate_id,
644 &candidate_location,
645 candidate_asn,
646 &candidate_region,
647 &existing_same_asn,
648 );
649 assert!(factor < 1.0); }
651
652 #[test]
653 fn test_diversity_validation() {
654 let enforcer = DiversityEnforcer::new();
655
656 let valid_selection = vec![
658 (
659 NodeId::from_bytes([1u8; 32]),
660 create_test_location(40.7128, -74.0060),
661 12345,
662 NetworkRegion::NorthAmerica,
663 ), (
665 NodeId::from_bytes([2u8; 32]),
666 create_test_location(34.0522, -118.2437),
667 54321,
668 NetworkRegion::NorthAmerica,
669 ), ];
671 assert!(enforcer.validate_selection(&valid_selection).is_ok());
672
673 let too_close_selection = vec![
675 (
676 NodeId::from_bytes([1u8; 32]),
677 create_test_location(40.7128, -74.0060),
678 12345,
679 NetworkRegion::NorthAmerica,
680 ), (
682 NodeId::from_bytes([2u8; 32]),
683 create_test_location(40.7589, -73.9851),
684 54321,
685 NetworkRegion::NorthAmerica,
686 ), ];
688 assert!(enforcer.validate_selection(&too_close_selection).is_err());
689 }
690}