@@ -43,15 +43,32 @@ fn default_true() -> bool {
4343 true
4444}
4545
46+ /// Weight entry with hotkey (challenge returns this format)
47+ #[ derive( Clone , Debug , Serialize , Deserialize ) ]
48+ pub struct HotkeyWeightEntry {
49+ /// Miner hotkey (SS58 address)
50+ pub hotkey : String ,
51+ /// Weight (0.0 - 1.0, normalized)
52+ pub weight : f64 ,
53+ }
54+
4655/// Weights response from challenge endpoint
56+ ///
57+ /// Challenges return weights with hotkeys, not UIDs.
58+ /// The collector converts hotkeys to UIDs using the metagraph.
4759#[ derive( Clone , Debug , Serialize , Deserialize ) ]
4860pub struct ChallengeWeightsResponse {
4961 /// Epoch these weights are for
5062 pub epoch : u64 ,
51- /// UIDs to set weights for
63+ /// Weights per miner hotkey (preferred format)
64+ #[ serde( default ) ]
65+ pub weights : Vec < HotkeyWeightEntry > ,
66+ /// Legacy: UIDs (if challenge already converted)
67+ #[ serde( default ) ]
5268 pub uids : Vec < u16 > ,
53- /// Corresponding weights (0-65535 normalized)
54- pub weights : Vec < u16 > ,
69+ /// Legacy: Corresponding weights in u16 format
70+ #[ serde( default , rename = "weight_values" ) ]
71+ pub weight_values : Vec < u16 > ,
5572 /// Optional: challenge name
5673 #[ serde( default ) ]
5774 pub challenge_name : Option < String > ,
@@ -60,6 +77,12 @@ pub struct ChallengeWeightsResponse {
6077 pub mechanism_id : Option < u8 > ,
6178}
6279
80+ /// UID 0 is the burn address - weights for unknown hotkeys go here
81+ pub const BURN_UID : u16 = 0 ;
82+
83+ /// Maximum weight value for Bittensor
84+ pub const MAX_WEIGHT : u16 = 65535 ;
85+
6386/// Result of fetching weights from a single challenge
6487#[ derive( Clone , Debug ) ]
6588pub struct ChallengeWeightResult {
@@ -153,6 +176,93 @@ impl ChallengeWeightCollector {
153176 self . status . read ( ) . await . clone ( )
154177 }
155178
179+ /// Convert hotkey weights to UID weights using metagraph
180+ ///
181+ /// - Hotkeys found in metagraph get their corresponding UIDs
182+ /// - Hotkeys NOT found have their weight accumulated to UID 0 (burn)
183+ /// - Returns (uids, weights) in Bittensor u16 format
184+ fn convert_hotkeys_to_uids (
185+ & self ,
186+ hotkey_weights : & [ HotkeyWeightEntry ] ,
187+ ) -> ( Vec < u16 > , Vec < u16 > ) {
188+ if hotkey_weights. is_empty ( ) {
189+ return ( vec ! [ BURN_UID ] , vec ! [ MAX_WEIGHT ] ) ;
190+ }
191+
192+ let mut uid_weight_map: std:: collections:: HashMap < u16 , u64 > =
193+ std:: collections:: HashMap :: new ( ) ;
194+ let mut burn_weight: f64 = 0.0 ;
195+ let mut resolved_count = 0 ;
196+ let mut unresolved_count = 0 ;
197+
198+ for entry in hotkey_weights {
199+ // Look up UID from metagraph
200+ if let Some ( uid) = self . client . get_uid_for_hotkey ( & entry. hotkey ) {
201+ // Skip UID 0 from challenge weights - it's reserved for burn
202+ if uid == BURN_UID {
203+ debug ! (
204+ "Hotkey {} resolved to UID 0 (burn), adding to burn weight" ,
205+ entry. hotkey
206+ ) ;
207+ burn_weight += entry. weight ;
208+ } else {
209+ let weight_u16 = ( entry. weight . clamp ( 0.0 , 1.0 ) * MAX_WEIGHT as f64 ) as u64 ;
210+ * uid_weight_map. entry ( uid) . or_insert ( 0 ) += weight_u16;
211+ resolved_count += 1 ;
212+ }
213+ } else {
214+ // Hotkey not found in metagraph - add weight to burn (UID 0)
215+ warn ! (
216+ "Hotkey {} not found in metagraph, adding {:.4} weight to burn (UID 0)" ,
217+ entry. hotkey, entry. weight
218+ ) ;
219+ burn_weight += entry. weight ;
220+ unresolved_count += 1 ;
221+ }
222+ }
223+
224+ // Calculate total weight to ensure normalization
225+ let total_assigned: u64 = uid_weight_map. values ( ) . sum ( ) ;
226+ let burn_weight_u16 = ( burn_weight. clamp ( 0.0 , 1.0 ) * MAX_WEIGHT as f64 ) as u64 ;
227+
228+ // Build final vectors
229+ let mut uids = Vec :: with_capacity ( uid_weight_map. len ( ) + 1 ) ;
230+ let mut weights = Vec :: with_capacity ( uid_weight_map. len ( ) + 1 ) ;
231+
232+ // Add burn weight first if any
233+ let final_burn = if burn_weight_u16 > 0 || unresolved_count > 0 {
234+ // Ensure we don't exceed MAX_WEIGHT total
235+ let remaining = MAX_WEIGHT as u64 - total_assigned. min ( MAX_WEIGHT as u64 ) ;
236+ remaining. min ( burn_weight_u16) as u16
237+ } else {
238+ 0
239+ } ;
240+
241+ if final_burn > 0 || uid_weight_map. is_empty ( ) {
242+ uids. push ( BURN_UID ) ;
243+ weights. push ( if uid_weight_map. is_empty ( ) {
244+ MAX_WEIGHT
245+ } else {
246+ final_burn
247+ } ) ;
248+ }
249+
250+ // Add resolved weights
251+ for ( uid, weight) in uid_weight_map {
252+ uids. push ( uid) ;
253+ weights. push ( weight. min ( MAX_WEIGHT as u64 ) as u16 ) ;
254+ }
255+
256+ info ! (
257+ "Converted {} hotkeys: {} resolved to UIDs, {} unresolved -> burn (UID 0)" ,
258+ hotkey_weights. len( ) ,
259+ resolved_count,
260+ unresolved_count
261+ ) ;
262+
263+ ( uids, weights)
264+ }
265+
156266 /// Fetch weights from a single challenge endpoint
157267 async fn fetch_challenge_weights (
158268 & self ,
@@ -193,32 +303,43 @@ impl ChallengeWeightCollector {
193303 let duration_ms = start. elapsed ( ) . as_millis ( ) as u64 ;
194304
195305 match result {
196- Ok ( Ok ( weights) ) => {
197- // Verify weights
198- if weights. uids . len ( ) != weights. weights . len ( ) {
199- return ChallengeWeightResult {
200- mechanism_id : endpoint. mechanism_id ,
201- challenge_name : endpoint. name . clone ( ) ,
202- uids : vec ! [ ] ,
203- weights : vec ! [ ] ,
204- success : false ,
205- error : Some ( "UIDs and weights length mismatch" . to_string ( ) ) ,
206- duration_ms,
207- } ;
208- }
306+ Ok ( Ok ( response) ) => {
307+ // Check if challenge returned hotkey-based weights (preferred format)
308+ let ( uids, weights) = if !response. weights . is_empty ( ) {
309+ // Convert hotkeys to UIDs using metagraph
310+ self . convert_hotkeys_to_uids ( & response. weights )
311+ } else if !response. uids . is_empty ( ) && !response. weight_values . is_empty ( ) {
312+ // Legacy format: challenge already provided UIDs
313+ if response. uids . len ( ) != response. weight_values . len ( ) {
314+ return ChallengeWeightResult {
315+ mechanism_id : endpoint. mechanism_id ,
316+ challenge_name : endpoint. name . clone ( ) ,
317+ uids : vec ! [ ] ,
318+ weights : vec ! [ ] ,
319+ success : false ,
320+ error : Some ( "UIDs and weights length mismatch" . to_string ( ) ) ,
321+ duration_ms,
322+ } ;
323+ }
324+ ( response. uids , response. weight_values )
325+ } else {
326+ // No weights returned - default to 100% burn
327+ warn ! ( "Challenge {} returned empty weights" , endpoint. name) ;
328+ ( vec ! [ BURN_UID ] , vec ! [ MAX_WEIGHT ] )
329+ } ;
209330
210331 info ! (
211- "Fetched {} weights from {} in {}ms" ,
212- weights. uids. len( ) ,
332+ "Fetched weights from {} in {}ms: {} entries" ,
213333 endpoint. name,
214- duration_ms
334+ duration_ms,
335+ uids. len( )
215336 ) ;
216337
217338 ChallengeWeightResult {
218339 mechanism_id : endpoint. mechanism_id ,
219340 challenge_name : endpoint. name . clone ( ) ,
220- uids : weights . uids ,
221- weights : weights . weights ,
341+ uids,
342+ weights,
222343 success : true ,
223344 error : None ,
224345 duration_ms,
@@ -359,11 +480,29 @@ impl ChallengeWeightCollector {
359480 Ok ( tx_hash)
360481 }
361482
483+ /// Sync metagraph before weight collection
484+ ///
485+ /// This MUST be called before collect_and_submit to ensure
486+ /// hotkeys can be converted to UIDs correctly.
487+ pub async fn sync_metagraph ( & mut self ) -> Result < ( ) > {
488+ info ! ( "Syncing metagraph for hotkey->UID conversion..." ) ;
489+ self . client . sync_metagraph ( ) . await ?;
490+
491+ if let Some ( metagraph) = self . client . metagraph ( ) {
492+ info ! ( "Metagraph synced: {} neurons" , metagraph. neurons. len( ) ) ;
493+ }
494+ Ok ( ( ) )
495+ }
496+
362497 /// Handle new epoch event
363498 ///
364- /// Called when a new epoch starts. Collects weights and submits.
365- pub async fn on_new_epoch ( & self , epoch : u64 ) -> Result < String > {
499+ /// Called when a new epoch starts. Syncs metagraph, collects weights and submits.
500+ pub async fn on_new_epoch ( & mut self , epoch : u64 ) -> Result < String > {
366501 info ! ( "New epoch {} - starting weight collection" , epoch) ;
502+
503+ // Sync metagraph to get latest hotkey->UID mappings
504+ self . sync_metagraph ( ) . await ?;
505+
367506 self . collect_and_submit ( epoch) . await
368507 }
369508
@@ -431,11 +570,56 @@ mod tests {
431570 }
432571
433572 #[ test]
434- fn test_weights_response_serde ( ) {
573+ fn test_hotkey_weight_entry_serde ( ) {
574+ let entry = HotkeyWeightEntry {
575+ hotkey : "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" . to_string ( ) ,
576+ weight : 0.5 ,
577+ } ;
578+
579+ let json = serde_json:: to_string ( & entry) . unwrap ( ) ;
580+ let parsed: HotkeyWeightEntry = serde_json:: from_str ( & json) . unwrap ( ) ;
581+
582+ assert_eq ! ( parsed. weight, 0.5 ) ;
583+ assert ! ( parsed. hotkey. starts_with( "5G" ) ) ;
584+ }
585+
586+ #[ test]
587+ fn test_weights_response_with_hotkeys ( ) {
588+ // New format: weights with hotkeys
589+ let response = ChallengeWeightsResponse {
590+ epoch : 100 ,
591+ weights : vec ! [
592+ HotkeyWeightEntry {
593+ hotkey: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" . to_string( ) ,
594+ weight: 0.6 ,
595+ } ,
596+ HotkeyWeightEntry {
597+ hotkey: "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" . to_string( ) ,
598+ weight: 0.4 ,
599+ } ,
600+ ] ,
601+ uids : vec ! [ ] ,
602+ weight_values : vec ! [ ] ,
603+ challenge_name : Some ( "Test" . to_string ( ) ) ,
604+ mechanism_id : Some ( 1 ) ,
605+ } ;
606+
607+ let json = serde_json:: to_string ( & response) . unwrap ( ) ;
608+ let parsed: ChallengeWeightsResponse = serde_json:: from_str ( & json) . unwrap ( ) ;
609+
610+ assert_eq ! ( parsed. epoch, 100 ) ;
611+ assert_eq ! ( parsed. weights. len( ) , 2 ) ;
612+ assert ! ( parsed. uids. is_empty( ) ) ;
613+ }
614+
615+ #[ test]
616+ fn test_weights_response_legacy_format ( ) {
617+ // Legacy format: UIDs directly
435618 let response = ChallengeWeightsResponse {
436619 epoch : 100 ,
620+ weights : vec ! [ ] ,
437621 uids : vec ! [ 1 , 2 , 3 ] ,
438- weights : vec ! [ 20000 , 30000 , 15535 ] ,
622+ weight_values : vec ! [ 20000 , 30000 , 15535 ] ,
439623 challenge_name : Some ( "Test" . to_string ( ) ) ,
440624 mechanism_id : Some ( 1 ) ,
441625 } ;
@@ -445,5 +629,6 @@ mod tests {
445629
446630 assert_eq ! ( parsed. epoch, 100 ) ;
447631 assert_eq ! ( parsed. uids. len( ) , 3 ) ;
632+ assert_eq ! ( parsed. weight_values. len( ) , 3 ) ;
448633 }
449634}
0 commit comments