Skip to content

Commit 168f138

Browse files
authored
fix(weights): normalize challenge weights when sum exceeds 1.0 (#34)
Add proportional normalization for challenge weights to prevent any single challenge from exceeding its allocated weight share. When a challenge returns weights that sum to more than 1.0, each weight is scaled down proportionally to sum exactly to 1.0. - Add normalize_hotkey_weights() function - Integrate normalization in collect_challenge_weights() - Add comprehensive unit tests for edge cases
1 parent 0f51b83 commit 168f138

File tree

1 file changed

+260
-1
lines changed

1 file changed

+260
-1
lines changed

crates/bittensor-integration/src/challenge_weight_collector.rs

Lines changed: 260 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,41 @@ pub const BURN_UID: u16 = 0;
8484
/// Maximum weight value for Bittensor
8585
pub const MAX_WEIGHT: u16 = 65535;
8686

87+
/// Normalize hotkey weights proportionally if their sum exceeds 1.0
88+
///
89+
/// When a challenge returns weights that sum to more than 1.0, each weight
90+
/// is scaled down proportionally so the total equals 1.0. This ensures
91+
/// no challenge can exceed its allocated weight share.
92+
///
93+
/// - If sum > 1.0: all weights are scaled by (1.0 / sum)
94+
/// - If sum <= 1.0: weights are returned unchanged
95+
fn normalize_hotkey_weights(weights: Vec<HotkeyWeightEntry>) -> Vec<HotkeyWeightEntry> {
96+
if weights.is_empty() {
97+
return weights;
98+
}
99+
100+
let sum: f64 = weights.iter().map(|w| w.weight).sum();
101+
102+
// Only normalize if sum exceeds 1.0
103+
if sum > 1.0 {
104+
tracing::info!(
105+
"Normalizing {} weights: sum={:.4} -> 1.0 (scaling by {:.4})",
106+
weights.len(),
107+
sum,
108+
1.0 / sum
109+
);
110+
weights
111+
.into_iter()
112+
.map(|w| HotkeyWeightEntry {
113+
hotkey: w.hotkey,
114+
weight: w.weight / sum,
115+
})
116+
.collect()
117+
} else {
118+
weights
119+
}
120+
}
121+
87122
/// Result of fetching weights from a single challenge
88123
#[derive(Clone, Debug)]
89124
pub struct ChallengeWeightResult {
@@ -318,8 +353,10 @@ impl ChallengeWeightCollector {
318353
Ok(Ok(response)) => {
319354
// Check if challenge returned hotkey-based weights (preferred format)
320355
let (uids, weights) = if !response.weights.is_empty() {
356+
// Normalize weights if sum > 1.0 to prevent exceeding allocation
357+
let normalized_weights = normalize_hotkey_weights(response.weights);
321358
// Convert hotkeys to UIDs using metagraph
322-
self.convert_hotkeys_to_uids(&response.weights)
359+
self.convert_hotkeys_to_uids(&normalized_weights)
323360
} else if !response.uids.is_empty() && !response.weight_values.is_empty() {
324361
// Legacy format: challenge already provided UIDs
325362
if response.uids.len() != response.weight_values.len() {
@@ -843,4 +880,226 @@ mod tests {
843880
}]);
844881
assert_eq!(uids, vec![12]);
845882
}
883+
884+
// Tests for normalize_hotkey_weights
885+
886+
#[test]
887+
fn test_normalize_hotkey_weights_sum_greater_than_one() {
888+
// Weights sum to 2.0, should be scaled to sum to 1.0
889+
let weights = vec![
890+
HotkeyWeightEntry {
891+
hotkey: "hk1".to_string(),
892+
weight: 1.2,
893+
},
894+
HotkeyWeightEntry {
895+
hotkey: "hk2".to_string(),
896+
weight: 0.8,
897+
},
898+
];
899+
900+
let normalized = normalize_hotkey_weights(weights);
901+
902+
assert_eq!(normalized.len(), 2);
903+
let sum: f64 = normalized.iter().map(|w| w.weight).sum();
904+
assert!(
905+
(sum - 1.0).abs() < 0.0001,
906+
"Sum should be ~1.0, got {}",
907+
sum
908+
);
909+
910+
// Check proportions are preserved (1.2:0.8 = 60%:40%)
911+
let hk1 = normalized.iter().find(|w| w.hotkey == "hk1").unwrap();
912+
let hk2 = normalized.iter().find(|w| w.hotkey == "hk2").unwrap();
913+
assert!(
914+
(hk1.weight - 0.6).abs() < 0.0001,
915+
"hk1 should be 0.6, got {}",
916+
hk1.weight
917+
);
918+
assert!(
919+
(hk2.weight - 0.4).abs() < 0.0001,
920+
"hk2 should be 0.4, got {}",
921+
hk2.weight
922+
);
923+
}
924+
925+
#[test]
926+
fn test_normalize_hotkey_weights_sum_equal_to_one() {
927+
// Weights sum to exactly 1.0, should remain unchanged
928+
let weights = vec![
929+
HotkeyWeightEntry {
930+
hotkey: "hk1".to_string(),
931+
weight: 0.7,
932+
},
933+
HotkeyWeightEntry {
934+
hotkey: "hk2".to_string(),
935+
weight: 0.3,
936+
},
937+
];
938+
939+
let normalized = normalize_hotkey_weights(weights);
940+
941+
assert_eq!(normalized.len(), 2);
942+
let hk1 = normalized.iter().find(|w| w.hotkey == "hk1").unwrap();
943+
let hk2 = normalized.iter().find(|w| w.hotkey == "hk2").unwrap();
944+
assert!(
945+
(hk1.weight - 0.7).abs() < 0.0001,
946+
"hk1 should remain 0.7, got {}",
947+
hk1.weight
948+
);
949+
assert!(
950+
(hk2.weight - 0.3).abs() < 0.0001,
951+
"hk2 should remain 0.3, got {}",
952+
hk2.weight
953+
);
954+
}
955+
956+
#[test]
957+
fn test_normalize_hotkey_weights_sum_less_than_one() {
958+
// Weights sum to 0.5, should remain unchanged (not inflated)
959+
let weights = vec![
960+
HotkeyWeightEntry {
961+
hotkey: "hk1".to_string(),
962+
weight: 0.3,
963+
},
964+
HotkeyWeightEntry {
965+
hotkey: "hk2".to_string(),
966+
weight: 0.2,
967+
},
968+
];
969+
970+
let normalized = normalize_hotkey_weights(weights);
971+
972+
assert_eq!(normalized.len(), 2);
973+
let sum: f64 = normalized.iter().map(|w| w.weight).sum();
974+
assert!(
975+
(sum - 0.5).abs() < 0.0001,
976+
"Sum should remain 0.5, got {}",
977+
sum
978+
);
979+
980+
let hk1 = normalized.iter().find(|w| w.hotkey == "hk1").unwrap();
981+
let hk2 = normalized.iter().find(|w| w.hotkey == "hk2").unwrap();
982+
assert!(
983+
(hk1.weight - 0.3).abs() < 0.0001,
984+
"hk1 should remain 0.3, got {}",
985+
hk1.weight
986+
);
987+
assert!(
988+
(hk2.weight - 0.2).abs() < 0.0001,
989+
"hk2 should remain 0.2, got {}",
990+
hk2.weight
991+
);
992+
}
993+
994+
#[test]
995+
fn test_normalize_hotkey_weights_empty() {
996+
let weights: Vec<HotkeyWeightEntry> = vec![];
997+
let normalized = normalize_hotkey_weights(weights);
998+
assert!(normalized.is_empty());
999+
}
1000+
1001+
#[test]
1002+
fn test_normalize_hotkey_weights_single_entry_above_one() {
1003+
// Single weight of 1.5 should be scaled to 1.0
1004+
let weights = vec![HotkeyWeightEntry {
1005+
hotkey: "hk1".to_string(),
1006+
weight: 1.5,
1007+
}];
1008+
1009+
let normalized = normalize_hotkey_weights(weights);
1010+
1011+
assert_eq!(normalized.len(), 1);
1012+
assert!(
1013+
(normalized[0].weight - 1.0).abs() < 0.0001,
1014+
"Single weight should be scaled to 1.0, got {}",
1015+
normalized[0].weight
1016+
);
1017+
}
1018+
1019+
#[test]
1020+
fn test_normalize_hotkey_weights_preserves_relative_proportions() {
1021+
// Weights with varying magnitudes: sum = 3.0
1022+
let weights = vec![
1023+
HotkeyWeightEntry {
1024+
hotkey: "hk1".to_string(),
1025+
weight: 1.5, // 50%
1026+
},
1027+
HotkeyWeightEntry {
1028+
hotkey: "hk2".to_string(),
1029+
weight: 0.9, // 30%
1030+
},
1031+
HotkeyWeightEntry {
1032+
hotkey: "hk3".to_string(),
1033+
weight: 0.6, // 20%
1034+
},
1035+
];
1036+
1037+
let normalized = normalize_hotkey_weights(weights);
1038+
1039+
assert_eq!(normalized.len(), 3);
1040+
let sum: f64 = normalized.iter().map(|w| w.weight).sum();
1041+
assert!(
1042+
(sum - 1.0).abs() < 0.0001,
1043+
"Sum should be ~1.0, got {}",
1044+
sum
1045+
);
1046+
1047+
// Check proportions: 1.5:0.9:0.6 = 50%:30%:20%
1048+
let hk1 = normalized.iter().find(|w| w.hotkey == "hk1").unwrap();
1049+
let hk2 = normalized.iter().find(|w| w.hotkey == "hk2").unwrap();
1050+
let hk3 = normalized.iter().find(|w| w.hotkey == "hk3").unwrap();
1051+
1052+
assert!(
1053+
(hk1.weight - 0.5).abs() < 0.0001,
1054+
"hk1 should be 0.5, got {}",
1055+
hk1.weight
1056+
);
1057+
assert!(
1058+
(hk2.weight - 0.3).abs() < 0.0001,
1059+
"hk2 should be 0.3, got {}",
1060+
hk2.weight
1061+
);
1062+
assert!(
1063+
(hk3.weight - 0.2).abs() < 0.0001,
1064+
"hk3 should be 0.2, got {}",
1065+
hk3.weight
1066+
);
1067+
}
1068+
1069+
#[test]
1070+
fn test_normalize_hotkey_weights_very_large_sum() {
1071+
// Extreme case: weights sum to 10.0
1072+
let weights = vec![
1073+
HotkeyWeightEntry {
1074+
hotkey: "hk1".to_string(),
1075+
weight: 6.0,
1076+
},
1077+
HotkeyWeightEntry {
1078+
hotkey: "hk2".to_string(),
1079+
weight: 4.0,
1080+
},
1081+
];
1082+
1083+
let normalized = normalize_hotkey_weights(weights);
1084+
1085+
let sum: f64 = normalized.iter().map(|w| w.weight).sum();
1086+
assert!(
1087+
(sum - 1.0).abs() < 0.0001,
1088+
"Sum should be ~1.0, got {}",
1089+
sum
1090+
);
1091+
1092+
let hk1 = normalized.iter().find(|w| w.hotkey == "hk1").unwrap();
1093+
let hk2 = normalized.iter().find(|w| w.hotkey == "hk2").unwrap();
1094+
assert!(
1095+
(hk1.weight - 0.6).abs() < 0.0001,
1096+
"hk1 should be 0.6, got {}",
1097+
hk1.weight
1098+
);
1099+
assert!(
1100+
(hk2.weight - 0.4).abs() < 0.0001,
1101+
"hk2 should be 0.4, got {}",
1102+
hk2.weight
1103+
);
1104+
}
8461105
}

0 commit comments

Comments
 (0)