diff --git a/rs/decentralization/src/nakamoto/mod.rs b/rs/decentralization/src/nakamoto/mod.rs index b78bc42ab..97c771303 100644 --- a/rs/decentralization/src/nakamoto/mod.rs +++ b/rs/decentralization/src/nakamoto/mod.rs @@ -140,7 +140,17 @@ impl NakamotoScore { let only_counter = counters.iter().map(|(_feat, cnt)| *cnt).collect::>(); // But for deeper understanding (logging and debugging) we also keep track of // all strings and their counts - let value_counts = counters.into_iter().sorted_by_key(|(_feat, cnt)| -(*cnt as isize)).collect::>(); + let value_counts = counters + .into_iter() + .sorted_unstable_by(|(feat1, cnt1), (feat2, cnt2)| { + let cmp1 = cnt2.partial_cmp(cnt1).unwrap(); + if cmp1 == Ordering::Equal { + feat1.partial_cmp(feat2).unwrap_or(Ordering::Equal) + } else { + cmp1 + } + }) + .collect::>(); (value.0.clone(), Self::nakamoto(&only_counter), value_counts) }); @@ -696,8 +706,11 @@ mod tests { assert_eq!( subnet_initial.check_business_rules().unwrap(), ( - 1000, - vec!["NodeFeature 'country' controls 9 of nodes, which is > 8 (2/3 of all) nodes".to_string()] + 1070, + vec![ + "Country US controls 9 of nodes, which is higher than target of 2 for the subnet. Applying penalty of 70.".to_string(), + "NodeFeature country controls 9 of nodes, which is > 8 (2/3 of all) nodes".to_string() + ] ) ); let nodes_available = new_test_nodes_with_overrides("spare", 13, 3, 0, (&NodeFeature::Country, &["US", "RO", "JP"])); @@ -753,7 +766,13 @@ mod tests { ); assert_eq!( subnet_initial.check_business_rules().unwrap(), - (10000, vec!["A single Node Provider can halt the subnet".to_string()]) + ( + 10020, + vec![ + "node_provider NP2 controls 3 of nodes, which is higher than target of 1 for the subnet. Applying penalty of 20.".to_string(), + "A single Node Provider can halt the subnet".to_string() + ] + ) ); let nodes_available = new_test_nodes_with_overrides("spare", 7, 2, 0, (&NodeFeature::NodeProvider, &["NP6", "NP7"])); let health_of_nodes = nodes_available.iter().map(|n| (n.id, HealthStatus::Healthy)).collect::>(); @@ -800,7 +819,13 @@ mod tests { 1, (&NodeFeature::NodeProvider, &["NP1", "NP2", "NP2", "NP3", "NP4", "NP4", "NP5"]), ); - assert_eq!(subnet_initial.check_business_rules().unwrap(), (0, vec![])); + assert_eq!( + subnet_initial.check_business_rules().unwrap(), + ( + 10, + vec!["node_provider NP2 controls 2 of nodes, which is higher than target of 1 for the subnet. Applying penalty of 10.".to_string()] + ) + ); // There are 2 spare nodes, but both are DFINITY let nodes_available = new_test_nodes_with_overrides("spare", 7, 2, 2, (&NodeFeature::NodeProvider, &["NP6", "NP7"])); diff --git a/rs/decentralization/src/network.rs b/rs/decentralization/src/network.rs index 09fdec97f..178894833 100644 --- a/rs/decentralization/src/network.rs +++ b/rs/decentralization/src/network.rs @@ -320,17 +320,65 @@ impl DecentralizedSubnet { Some((country_dominant, country_nodes_count)) => { let controlled_nodes_max = nodes.len() / 3; if country_nodes_count > controlled_nodes_max { + let penalty = (country_nodes_count - controlled_nodes_max) * 1000; checks.push(format!( - "Country '{}' controls {} of nodes, which is > {} (1/3 - 1) of subnet nodes", - country_dominant, country_nodes_count, controlled_nodes_max + "Country {} controls {} of nodes, which is > {} (1/3 - 1) of subnet nodes. Applying penalty of {}.", + country_dominant, country_nodes_count, controlled_nodes_max, penalty )); - penalties += (country_nodes_count - controlled_nodes_max) * 1000; + penalties += penalty; } } _ => return Err(anyhow::anyhow!("Incomplete data for {}", feature)), } } + // As per the adopted target topology + // https://dashboard.internetcomputer.org/proposal/132136 + let max_nodes_per_np_and_dc = 1; + for feature in &[NodeFeature::NodeProvider, NodeFeature::DataCenter, NodeFeature::DataCenterOwner] { + match nakamoto_scores.feature_value_counts_max(feature) { + Some((name, value)) => { + if value > max_nodes_per_np_and_dc { + let penalty = (value - max_nodes_per_np_and_dc) * 10; + checks.push(format!( + "{} {} controls {} of nodes, which is higher than target of {} for the subnet. Applying penalty of {}.", + feature, name, value, max_nodes_per_np_and_dc, penalty + )); + penalties += penalty; + } + } + _ => return Err(anyhow::anyhow!("Incomplete data for {}", feature)), + } + } + + // As per the adopted target topology + // https://dashboard.internetcomputer.org/proposal/132136 + let max_nodes_per_country = match subnet_id_str.as_str() { + "tdb26-jop6k-aogll-7ltgs-eruif-6kk7m-qpktf-gdiqx-mxtrf-vb5e6-eqe" + | "x33ed-h457x-bsgyx-oqxqf-6pzwv-wkhzr-rm2j3-npodi-purzm-n66cg-gae" + | "pzp6e-ekpqk-3c5x7-2h6so-njoeq-mt45d-h3h6c-q3mxf-vpeq5-fk5o7-yae" + | "uzr34-akd3s-xrdag-3ql62-ocgoh-ld2ao-tamcv-54e7j-krwgb-2gm4z-oqe" => 3, + _ => 2, + }; + match nakamoto_scores.feature_value_counts_max(&NodeFeature::Country) { + Some((name, value)) => { + if value > max_nodes_per_country { + let penalty = (value - max_nodes_per_country) * 10; + checks.push(format!( + "Country {} controls {} of nodes, which is higher than target of {} for the subnet. Applying penalty of {}.", + name, value, max_nodes_per_country, penalty + )); + penalties += penalty; + } + } + _ => { + return Err(anyhow::anyhow!( + "Incomplete data for Node Feature Country in subnet {}", + subnet_id.to_string() + )) + } + } + if is_european_subnet { // European subnet should only take European nodes. let continent_counts = nakamoto_scores.feature_value_counts(&NodeFeature::Continent); @@ -387,7 +435,7 @@ impl DecentralizedSubnet { if score == 1.0 && controlled_nodes > nodes.len() * 2 / 3 && !european_subnet_continent_penalty { checks.push(format!( - "NodeFeature '{}' controls {} of nodes, which is > {} (2/3 of all) nodes", + "NodeFeature {} controls {} of nodes, which is > {} (2/3 of all) nodes", feature, controlled_nodes, nodes.len() * 2 / 3