Skip to content

Commit 319f92d

Browse files
committed
feat: auto-convert hotkeys to UIDs in weight submission
- challenge_weight_collector.rs: Add HotkeyWeightEntry type for hotkey-based weights - challenge_weight_collector.rs: Implement convert_hotkeys_to_uids() using metagraph - challenge_weight_collector.rs: Unknown hotkeys have their weight sent to burn (UID 0) - challenge_weight_collector.rs: Auto-sync metagraph before weight collection - runtime.rs: Add collect_and_get_weights_with_mapping() for explicit UID mapping - mechanism_weights.rs: Add warning when using MechanismWeights without hotkey mapping
1 parent def9cc8 commit 319f92d

File tree

3 files changed

+269
-35
lines changed

3 files changed

+269
-35
lines changed

crates/bittensor-integration/src/challenge_weight_collector.rs

Lines changed: 210 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
4860
pub 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)]
6588
pub 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
}

crates/challenge-runtime/src/runtime.rs

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,23 @@ impl ChallengeRuntime {
536536
///
537537
/// This is the primary method for event-driven weight submission.
538538
/// Called when Bittensor CommitWindowOpen event fires.
539+
///
540+
/// # Arguments
541+
/// * `hotkey_to_uid` - Optional mapping from SS58 hotkey to Bittensor UID.
542+
/// If None, weights are returned with sequential UIDs (for testing only).
543+
/// In production, pass the metagraph mapping from SubtensorClient::get_uids_for_hotkeys().
539544
pub async fn collect_and_get_weights(&self) -> Vec<(u8, Vec<u16>, Vec<u16>)> {
545+
self.collect_and_get_weights_with_mapping(None).await
546+
}
547+
548+
/// Collect weights with explicit hotkey->UID mapping from metagraph
549+
///
550+
/// # Arguments
551+
/// * `hotkey_to_uid` - Mapping from SS58 hotkey to Bittensor UID from metagraph
552+
pub async fn collect_and_get_weights_with_mapping(
553+
&self,
554+
hotkey_to_uid: Option<&HashMap<String, u16>>,
555+
) -> Vec<(u8, Vec<u16>, Vec<u16>)> {
540556
let epoch = self.epoch_manager.current_epoch();
541557
info!("Collecting weights for epoch {} (event-driven)", epoch);
542558

@@ -577,14 +593,41 @@ impl ChallengeRuntime {
577593
);
578594

579595
// Convert to Bittensor format (uid, weight as u16)
580-
// For now, use placeholder UIDs - actual UID mapping would come from metagraph
581-
let uids: Vec<u16> = (0..weights.len() as u16).collect();
582-
let weight_values: Vec<u16> = weights
583-
.iter()
584-
.map(|w| (w.weight.clamp(0.0, 1.0) * 65535.0) as u16)
585-
.collect();
586-
587-
result.push((mechanism_id, uids, weight_values));
596+
let (uids, weight_values): (Vec<u16>, Vec<u16>) = if let Some(mapping) =
597+
hotkey_to_uid
598+
{
599+
// Use metagraph mapping to convert hotkeys to UIDs
600+
weights
601+
.iter()
602+
.filter_map(|w| {
603+
mapping.get(&w.hotkey).map(|uid| {
604+
let weight_u16 = (w.weight.clamp(0.0, 1.0) * 65535.0) as u16;
605+
(*uid, weight_u16)
606+
})
607+
})
608+
.unzip()
609+
} else {
610+
// Fallback: use sequential UIDs (for testing only)
611+
warn!(
612+
"No hotkey->UID mapping provided, using sequential UIDs (testing mode)"
613+
);
614+
let uids: Vec<u16> = (0..weights.len() as u16).collect();
615+
let weight_values: Vec<u16> = weights
616+
.iter()
617+
.map(|w| (w.weight.clamp(0.0, 1.0) * 65535.0) as u16)
618+
.collect();
619+
(uids, weight_values)
620+
};
621+
622+
if !uids.is_empty() {
623+
result.push((mechanism_id, uids, weight_values));
624+
} else {
625+
warn!(
626+
"Challenge {} has {} weights but no valid UIDs found in mapping",
627+
challenge_id,
628+
weights.len()
629+
);
630+
}
588631
}
589632
Err(e) => {
590633
error!(

0 commit comments

Comments
 (0)