@@ -84,6 +84,41 @@ pub const BURN_UID: u16 = 0;
8484/// Maximum weight value for Bittensor
8585pub 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 ) ]
89124pub 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