Skip to content

Commit dcc363f

Browse files
committed
fix(validator): correct weight merging for hotkeys without UIDs
Complete rewrite of weight submission logic to properly handle: - Hotkeys without UIDs in metagraph -> sent to burn (UID 0) - Multiple challenges sharing same mechanism_id -> properly merged - Empty/error weight responses -> emission sent to burn - Normalization and max-upscaling done on f64 before u16 conversion Key changes: - Use f64 for accumulation instead of u32 to avoid precision loss - Hotkeys not in metagraph immediately add their weight to UID 0 - Challenges with empty/error weights send their emission_weight to burn - Final normalization ensures weights sum to 1.0 before max-upscaling - Better logging with resolved/unresolved hotkey counts
1 parent a6af2d5 commit dcc363f

File tree

1 file changed

+73
-53
lines changed

1 file changed

+73
-53
lines changed

bins/validator-node/src/main.rs

Lines changed: 73 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -915,9 +915,9 @@ async fn handle_block_event(
915915
// We must merge weights by mechanism before submitting
916916
let challenges = cached_challenges.read().clone();
917917
let mechanism_weights = if !challenges.is_empty() {
918-
// Collect weights per mechanism: HashMap<mechanism_id, HashMap<uid, weight_u32>>
919-
// Using u32 for intermediate accumulation to avoid overflow
920-
let mut mechanism_uid_weights: HashMap<u8, HashMap<u16, u32>> = HashMap::new();
918+
// Collect weights per mechanism using f64 for accurate accumulation
919+
// HashMap<mechanism_id, HashMap<uid, weight_f64>>
920+
let mut mechanism_uid_weights: HashMap<u8, HashMap<u16, f64>> = HashMap::new();
921921

922922
for challenge in challenges.iter() {
923923
// Skip unhealthy challenges - don't send burn, just skip
@@ -930,118 +930,138 @@ async fn handle_block_event(
930930
continue;
931931
}
932932

933+
let mech_id = challenge.mechanism_id as u8;
934+
let emission_weight = challenge.emission_weight.clamp(0.0, 1.0);
935+
933936
match platform_client.get_weights(&challenge.id, epoch).await {
934937
Ok(w) if !w.is_empty() => {
935-
// Get challenge emission weight (0.0-1.0)
936-
let emission_weight = challenge.emission_weight.clamp(0.0, 1.0);
937-
let mech_id = challenge.mechanism_id as u8;
938-
939938
// Get or create the UID->weight map for this mechanism
940939
let uid_weights = mechanism_uid_weights.entry(mech_id).or_default();
941940

942941
// Convert hotkeys to UIDs using metagraph
943942
let client_guard = client.read();
944-
let mut total_weight: f64 = 0.0;
943+
let mut resolved_count = 0usize;
944+
let mut unresolved_count = 0usize;
945945

946946
for (hotkey, weight_f64) in &w {
947-
// Apply emission_weight: scale the challenge weight
947+
// Scale by emission_weight: this challenge's share
948948
let scaled_weight = weight_f64 * emission_weight;
949949

950950
if let Some(uid) = client_guard.get_uid_for_hotkey(hotkey) {
951-
// Convert f64 weight (0.0-1.0) to u16 (0-65535)
952-
let weight_u16 = (scaled_weight * 65535.0).round() as u16;
953-
// Accumulate weight for this UID
954-
*uid_weights.entry(uid).or_insert(0) += weight_u16 as u32;
955-
total_weight += scaled_weight;
951+
// Accumulate f64 weight for this UID
952+
*uid_weights.entry(uid).or_insert(0.0) += scaled_weight;
953+
resolved_count += 1;
956954
info!(
957-
" [{}] {} -> UID {} (weight: {:.4} * {:.2} = {:.4} = {})",
955+
" [{}] {} -> UID {} (weight: {:.4} * {:.2} = {:.4})",
958956
challenge.id,
959957
&hotkey[..16],
960958
uid,
961959
weight_f64,
962960
emission_weight,
963-
scaled_weight,
964-
weight_u16
961+
scaled_weight
965962
);
966963
} else {
967-
// Hotkey not in metagraph - this weight goes to burn
964+
// Hotkey not in metagraph - add to burn (UID 0)
965+
*uid_weights.entry(0).or_insert(0.0) += scaled_weight;
966+
unresolved_count += 1;
968967
warn!(
969-
"Hotkey {} not found in metagraph, weight {:.4} goes to burn",
968+
" [{}] {} not in metagraph -> UID 0 (burn: {:.4})",
969+
challenge.id,
970970
&hotkey[..16],
971971
scaled_weight
972972
);
973-
// Don't add to total_weight - it will be added to burn below
974973
}
975974
}
976975
drop(client_guard);
977976

978-
// Add remaining weight to burn (UID 0)
979-
// This includes: (1 - emission_weight) + any unresolved hotkey weights
980-
let burn_weight = 1.0 - total_weight;
981-
if burn_weight > 0.001 {
982-
let burn_u16 = (burn_weight * 65535.0).round() as u16;
983-
*uid_weights.entry(0).or_insert(0) += burn_u16 as u32;
977+
if unresolved_count > 0 {
978+
warn!(
979+
"Challenge {}: {} hotkeys resolved, {} unresolved (sent to burn)",
980+
challenge.id, resolved_count, unresolved_count
981+
);
982+
}
983+
984+
// Add (1 - sum_of_weights) * emission_weight to burn
985+
// This handles the case where challenge weights don't sum to 1.0
986+
let weights_sum: f64 = w.iter().map(|(_, w)| w).sum();
987+
let unallocated = (1.0 - weights_sum.min(1.0)) * emission_weight;
988+
if unallocated > 0.001 {
989+
*uid_weights.entry(0).or_insert(0.0) += unallocated;
984990
info!(
985-
" [{}] Burn (UID 0): {:.4} = {}",
986-
challenge.id, burn_weight, burn_u16
991+
" [{}] Unallocated -> UID 0 (burn: {:.4})",
992+
challenge.id, unallocated
987993
);
988994
}
989995

990996
info!(
991-
"Challenge {} (mech {}, emission_weight={:.2}): collected weights",
992-
challenge.id, challenge.mechanism_id, emission_weight
997+
"Challenge {} (mech {}, emission={:.2}): collected weights",
998+
challenge.id, mech_id, emission_weight
993999
);
9941000
}
9951001
Ok(_) => {
996-
// No weights returned - skip, chain keeps existing weights
1002+
// No weights returned - send this challenge's emission to burn
1003+
let uid_weights = mechanism_uid_weights.entry(mech_id).or_default();
1004+
*uid_weights.entry(0).or_insert(0.0) += emission_weight;
9971005
info!(
998-
"Challenge {} returned empty weights - skipping (chain keeps existing)",
999-
challenge.id
1006+
"Challenge {} returned empty weights - {:.4} burn to UID 0",
1007+
challenge.id, emission_weight
10001008
);
10011009
}
10021010
Err(e) => {
1003-
// Error fetching weights - skip, chain keeps existing weights
1004-
// Don't send burn - let chain maintain current state
1011+
// Error fetching weights - send this challenge's emission to burn
1012+
let uid_weights = mechanism_uid_weights.entry(mech_id).or_default();
1013+
*uid_weights.entry(0).or_insert(0.0) += emission_weight;
10051014
warn!(
1006-
"Failed to get weights for {} - skipping (chain keeps existing): {}",
1007-
challenge.id, e
1015+
"Failed to get weights for {} - {:.4} burn to UID 0: {}",
1016+
challenge.id, emission_weight, e
10081017
);
10091018
}
10101019
}
10111020
}
10121021

1013-
// Convert HashMap<mechanism_id, HashMap<uid, weight>> to Vec<(mech, uids, weights)>
1014-
// Apply max-upscaling per mechanism
1022+
// Convert HashMap<mechanism_id, HashMap<uid, weight_f64>> to Vec<(mech, uids, weights_u16)>
10151023
let mut weights: Vec<(u8, Vec<u16>, Vec<u16>)> = Vec::new();
10161024

10171025
for (mech_id, uid_weights) in mechanism_uid_weights {
10181026
if uid_weights.is_empty() {
10191027
continue;
10201028
}
10211029

1030+
// Normalize weights to sum to 1.0, then convert to u16
1031+
let total: f64 = uid_weights.values().sum();
1032+
if total <= 0.0 {
1033+
// Should not happen, but fallback to 100% burn
1034+
warn!(
1035+
"Mechanism {} has zero total weight - sending 100% burn",
1036+
mech_id
1037+
);
1038+
weights.push((mech_id, vec![0u16], vec![65535u16]));
1039+
continue;
1040+
}
1041+
10221042
let uids: Vec<u16> = uid_weights.keys().copied().collect();
1023-
let mut vals: Vec<u16> = uids
1043+
let vals_f64: Vec<f64> = uids
10241044
.iter()
1025-
.map(|uid| {
1026-
// Clamp accumulated value to u16 max
1027-
uid_weights.get(uid).copied().unwrap_or(0).min(65535) as u16
1028-
})
1045+
.map(|uid| uid_weights.get(uid).copied().unwrap_or(0.0) / total)
10291046
.collect();
10301047

1031-
// Max-upscale weights so largest = 65535
1048+
// Max-upscale: largest weight becomes 65535
10321049
// This matches Python's convert_weights_and_uids_for_emit behavior
1033-
let max_val = *vals.iter().max().unwrap_or(&0) as f64;
1034-
if max_val > 0.0 && max_val < 65535.0 {
1035-
vals = vals
1050+
let max_val = vals_f64.iter().cloned().fold(0.0_f64, f64::max);
1051+
let vals: Vec<u16> = if max_val > 0.0 {
1052+
vals_f64
10361053
.iter()
1037-
.map(|v| ((*v as f64 / max_val) * 65535.0).round() as u16)
1038-
.collect();
1039-
}
1054+
.map(|v| ((v / max_val) * 65535.0).round() as u16)
1055+
.collect()
1056+
} else {
1057+
vec![0u16; uids.len()]
1058+
};
10401059

10411060
info!(
1042-
"Mechanism {}: {} UIDs merged from all challenges (max-upscaled)",
1061+
"Mechanism {}: {} UIDs, total_weight={:.4} (normalized & max-upscaled)",
10431062
mech_id,
1044-
uids.len()
1063+
uids.len(),
1064+
total
10451065
);
10461066
debug!(" UIDs: {:?}, Weights: {:?}", uids, vals);
10471067
weights.push((mech_id, uids, vals));

0 commit comments

Comments
 (0)