From c87ba26a6ed86a05384421ca2cc24120c95fa872 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Thu, 11 Jun 2026 18:18:36 +0000 Subject: [PATCH 1/9] feat: allow theoretically unlimited number of gen4 nodes --- .../mutations/node_management/do_add_node.rs | 123 +++++++++++++++--- 1 file changed, 102 insertions(+), 21 deletions(-) diff --git a/rs/registry/canister/src/mutations/node_management/do_add_node.rs b/rs/registry/canister/src/mutations/node_management/do_add_node.rs index f5bc0c26a2df..08e67210d625 100644 --- a/rs/registry/canister/src/mutations/node_management/do_add_node.rs +++ b/rs/registry/canister/src/mutations/node_management/do_add_node.rs @@ -136,27 +136,42 @@ impl Registry { "{LOG_PREFIX}do_add_node: Node reward type is required." ))?; - let max_rewardable_nodes_same_type = *node_operator_record - .max_rewardable_nodes - .get(&(node_reward_type.to_string())) - .ok_or(format!("{LOG_PREFIX}do_add_node: Node Operator does not have rewardable nodes for {node_reward_type}"))?; - - let num_in_registry_same_type = get_node_operator_nodes(self, caller_id) - .into_iter() - .filter_map(|node| node.node_reward_type) - .filter(|t| t == &(node_reward_type as i32)) - .count() as u32; - - // Validate node operator's max_rewardable_nodes quota - if max_rewardable_nodes_same_type - <= num_in_registry_same_type.saturating_sub(num_removed_same_ip_same_type) - { - return Err(format!( - "{LOG_PREFIX}do_add_node: Node Operator has reached max_rewardable_nodes quota for {node_reward_type}.\ - Number of nodes in the registry with {node_reward_type} type = {num_in_registry_same_type},\ - Number of removed nodes with same IP and same type = {num_removed_same_ip_same_type},\ - {node_reward_type} quota = {max_rewardable_nodes_same_type}" - )); + // TODO(NNS1-XXXX): Re-enable the max_rewardable_nodes quota check for + // type4.1-type4.4 once we no longer treat rewards of type4.5 as type1.1. + // For now, node providers may deploy arbitrarily many nodes of these + // types without being constrained by `max_rewardable_nodes`. Type4.5 is + // explicitly excluded from this exemption. + let bypass_max_rewardable_nodes_check = matches!( + node_reward_type, + NodeRewardType::Type4dot1 + | NodeRewardType::Type4dot2 + | NodeRewardType::Type4dot3 + | NodeRewardType::Type4dot4 + ); + + if !bypass_max_rewardable_nodes_check { + let max_rewardable_nodes_same_type = *node_operator_record + .max_rewardable_nodes + .get(&(node_reward_type.to_string())) + .ok_or(format!("{LOG_PREFIX}do_add_node: Node Operator does not have rewardable nodes for {node_reward_type}"))?; + + let num_in_registry_same_type = get_node_operator_nodes(self, caller_id) + .into_iter() + .filter_map(|node| node.node_reward_type) + .filter(|t| t == &(node_reward_type as i32)) + .count() as u32; + + // Validate node operator's max_rewardable_nodes quota + if max_rewardable_nodes_same_type + <= num_in_registry_same_type.saturating_sub(num_removed_same_ip_same_type) + { + return Err(format!( + "{LOG_PREFIX}do_add_node: Node Operator has reached max_rewardable_nodes quota for {node_reward_type}.\ + Number of nodes in the registry with {node_reward_type} type = {num_in_registry_same_type},\ + Number of removed nodes with same IP and same type = {num_removed_same_ip_same_type},\ + {node_reward_type} quota = {max_rewardable_nodes_same_type}" + )); + } } } @@ -1124,6 +1139,72 @@ mod tests { .unwrap(); } + #[test] + fn should_allow_arbitrarily_many_nodes_of_type4dot1_through_type4dot4() { + // TODO(NNS1-XXXX): Remove this test once we re-enable the + // max_rewardable_nodes quota check for type4.1-type4.4. See related TODO + // in `do_add_node_`. + for node_reward_type in [ + NodeRewardType::Type4dot1, + NodeRewardType::Type4dot2, + NodeRewardType::Type4dot3, + NodeRewardType::Type4dot4, + ] { + let mut registry = invariant_compliant_registry(0); + + let (mutate_request, node_ids_and_dkg_pks) = prepare_registry_with_nodes(1, 1); + registry.maybe_apply_mutation_internal(mutate_request.mutations); + let node_ids: Vec = node_ids_and_dkg_pks.keys().cloned().collect(); + // Note: `max_rewardable_nodes` intentionally does NOT contain + // an entry for `node_reward_type` here, demonstrating that the + // quota does not apply for these types. + let node_operator_id = registry_add_node_operator_for_node( + &mut registry, + node_ids[0], + btreemap! {}, + ); + + // Adding several nodes of this type should all succeed even + // though no quota is configured. + for i in 0..3u8 { + let (payload, _) = prepare_add_node_payload(10 + i, node_reward_type); + registry + .do_add_node_(payload, node_operator_id, now_system_time()) + .unwrap_or_else(|e| { + panic!( + "do_add_node_ failed for {node_reward_type} on iteration {i}: {e}" + ) + }); + } + } + } + + #[test] + #[should_panic( + expected = "[Registry] do_add_node: Node Operator has reached max_rewardable_nodes quota for type4.5" + )] + fn should_panic_if_max_rewardable_nodes_is_exhausted_for_type4dot5() { + // type4.5 is explicitly excluded from the type4.1-type4.4 exemption + // because rewards of type4.5 are currently treated as type1.1. The + // standard max_rewardable_nodes quota check therefore still applies. + let mut registry = invariant_compliant_registry(0); + + let (mutate_request, node_ids_and_dkg_pks) = prepare_registry_with_nodes(1, 1); + registry.maybe_apply_mutation_internal(mutate_request.mutations); + let node_ids: Vec = node_ids_and_dkg_pks.keys().cloned().collect(); + let node_operator_id = registry_add_node_operator_for_node( + &mut registry, + node_ids[0], + btreemap! { NodeRewardType::Type4dot5 => 0 }, + ); + + let (payload, _valid_pks) = prepare_add_node_payload(2, NodeRewardType::Type4dot5); + + registry + .do_add_node_(payload, node_operator_id, now_system_time()) + .unwrap(); + } + #[test] fn test_node_reward_type_is_required() { let mut registry = invariant_compliant_registry(0); From 904c069358cef99a04d921ec6835954cf7349994 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Thu, 11 Jun 2026 18:20:50 +0000 Subject: [PATCH 2/9] comment --- .../canister/src/mutations/node_management/do_add_node.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/registry/canister/src/mutations/node_management/do_add_node.rs b/rs/registry/canister/src/mutations/node_management/do_add_node.rs index 08e67210d625..71966b3bee52 100644 --- a/rs/registry/canister/src/mutations/node_management/do_add_node.rs +++ b/rs/registry/canister/src/mutations/node_management/do_add_node.rs @@ -136,7 +136,7 @@ impl Registry { "{LOG_PREFIX}do_add_node: Node reward type is required." ))?; - // TODO(NNS1-XXXX): Re-enable the max_rewardable_nodes quota check for + // TODO(CLO-15): Re-enable the max_rewardable_nodes quota check for // type4.1-type4.4 once we no longer treat rewards of type4.5 as type1.1. // For now, node providers may deploy arbitrarily many nodes of these // types without being constrained by `max_rewardable_nodes`. Type4.5 is @@ -1141,7 +1141,7 @@ mod tests { #[test] fn should_allow_arbitrarily_many_nodes_of_type4dot1_through_type4dot4() { - // TODO(NNS1-XXXX): Remove this test once we re-enable the + // TODO(CLO-15): Remove this test once we re-enable the // max_rewardable_nodes quota check for type4.1-type4.4. See related TODO // in `do_add_node_`. for node_reward_type in [ From ad4fa5599ea18814df94a2d02edd32b8be4ca02c Mon Sep 17 00:00:00 2001 From: IDX GitHub Automation Date: Thu, 11 Jun 2026 18:29:23 +0000 Subject: [PATCH 3/9] Automatically fixing code for linting and formatting issues --- .../src/mutations/node_management/do_add_node.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/rs/registry/canister/src/mutations/node_management/do_add_node.rs b/rs/registry/canister/src/mutations/node_management/do_add_node.rs index 71966b3bee52..99550f2c13e9 100644 --- a/rs/registry/canister/src/mutations/node_management/do_add_node.rs +++ b/rs/registry/canister/src/mutations/node_management/do_add_node.rs @@ -1158,11 +1158,8 @@ mod tests { // Note: `max_rewardable_nodes` intentionally does NOT contain // an entry for `node_reward_type` here, demonstrating that the // quota does not apply for these types. - let node_operator_id = registry_add_node_operator_for_node( - &mut registry, - node_ids[0], - btreemap! {}, - ); + let node_operator_id = + registry_add_node_operator_for_node(&mut registry, node_ids[0], btreemap! {}); // Adding several nodes of this type should all succeed even // though no quota is configured. @@ -1171,9 +1168,7 @@ mod tests { registry .do_add_node_(payload, node_operator_id, now_system_time()) .unwrap_or_else(|e| { - panic!( - "do_add_node_ failed for {node_reward_type} on iteration {i}: {e}" - ) + panic!("do_add_node_ failed for {node_reward_type} on iteration {i}: {e}") }); } } From caf59aaefe664575e4435a99b1276f5d178c7945 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 12 Jun 2026 09:18:08 +0000 Subject: [PATCH 4/9] linting --- .../canister/src/mutations/node_management/do_add_node.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/registry/canister/src/mutations/node_management/do_add_node.rs b/rs/registry/canister/src/mutations/node_management/do_add_node.rs index 99550f2c13e9..ca758e08ccd7 100644 --- a/rs/registry/canister/src/mutations/node_management/do_add_node.rs +++ b/rs/registry/canister/src/mutations/node_management/do_add_node.rs @@ -1163,7 +1163,7 @@ mod tests { // Adding several nodes of this type should all succeed even // though no quota is configured. - for i in 0..3u8 { + for i in 0..3_u8 { let (payload, _) = prepare_add_node_payload(10 + i, node_reward_type); registry .do_add_node_(payload, node_operator_id, now_system_time()) From 8934f9f010cd4f6d1ae1a4a5d0000da84058d65e Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Wed, 17 Jun 2026 09:49:42 +0000 Subject: [PATCH 5/9] update unreleased change log --- rs/registry/canister/unreleased_changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rs/registry/canister/unreleased_changelog.md b/rs/registry/canister/unreleased_changelog.md index 93c14851bbe2..92d80ddadccd 100644 --- a/rs/registry/canister/unreleased_changelog.md +++ b/rs/registry/canister/unreleased_changelog.md @@ -24,6 +24,11 @@ on the process that this file is part of, see ## Changed +* Temporarily bypass the `max_rewardable_nodes` quota check in `add_node` for + node reward types `type4.1` through `type4.4`, allowing node providers to + register an arbitrary number of such nodes. `type4.5` is explicitly excluded + and still subject to the quota. This is a temporary measure until rewards of + `type4.5` are no longer treated as `type1.1` (see CLO-15). * One-time post-upgrade migration converting the reward type of 100 currently unassigned nodes from `type1.1` to `type4.5`. The migration only mutates nodes whose reward type is still `type1.1`, so it is idempotent across upgrades. From cbf6ec54febdc006574c5e848cf00fd5ae6e7a24 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Thu, 18 Jun 2026 11:48:48 +0000 Subject: [PATCH 6/9] addressing suggestions --- .../mutations/node_management/do_add_node.rs | 66 +++++++++++-------- rs/registry/canister/unreleased_changelog.md | 15 +++-- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/rs/registry/canister/src/mutations/node_management/do_add_node.rs b/rs/registry/canister/src/mutations/node_management/do_add_node.rs index ca758e08ccd7..026f2c0b9d6c 100644 --- a/rs/registry/canister/src/mutations/node_management/do_add_node.rs +++ b/rs/registry/canister/src/mutations/node_management/do_add_node.rs @@ -136,12 +136,15 @@ impl Registry { "{LOG_PREFIX}do_add_node: Node reward type is required." ))?; - // TODO(CLO-15): Re-enable the max_rewardable_nodes quota check for - // type4.1-type4.4 once we no longer treat rewards of type4.5 as type1.1. - // For now, node providers may deploy arbitrarily many nodes of these - // types without being constrained by `max_rewardable_nodes`. Type4.5 is - // explicitly excluded from this exemption. - let bypass_max_rewardable_nodes_check = matches!( + // TODO(CLO-15): Remove the type4.1-type4.4 special case below once + // we no longer treat rewards of type4.5 as type1.1. For now, node + // providers must be able to deploy arbitrarily many nodes of these + // types without being constrained by `max_rewardable_nodes`. Rather + // than branching around the check, we model this as an effectively + // unbounded quota so the same code path is exercised for every + // reward type. Type4.5 is explicitly excluded from this exemption. + const EXCESSIVE_NUMBER_OF_TYPE_4_NODES: u32 = 1_000; + let is_unbounded_type4 = matches!( node_reward_type, NodeRewardType::Type4dot1 | NodeRewardType::Type4dot2 @@ -149,29 +152,31 @@ impl Registry { | NodeRewardType::Type4dot4 ); - if !bypass_max_rewardable_nodes_check { - let max_rewardable_nodes_same_type = *node_operator_record + let max_rewardable_nodes_same_type = if is_unbounded_type4 { + EXCESSIVE_NUMBER_OF_TYPE_4_NODES + } else { + *node_operator_record .max_rewardable_nodes .get(&(node_reward_type.to_string())) - .ok_or(format!("{LOG_PREFIX}do_add_node: Node Operator does not have rewardable nodes for {node_reward_type}"))?; - - let num_in_registry_same_type = get_node_operator_nodes(self, caller_id) - .into_iter() - .filter_map(|node| node.node_reward_type) - .filter(|t| t == &(node_reward_type as i32)) - .count() as u32; - - // Validate node operator's max_rewardable_nodes quota - if max_rewardable_nodes_same_type - <= num_in_registry_same_type.saturating_sub(num_removed_same_ip_same_type) - { - return Err(format!( - "{LOG_PREFIX}do_add_node: Node Operator has reached max_rewardable_nodes quota for {node_reward_type}.\ - Number of nodes in the registry with {node_reward_type} type = {num_in_registry_same_type},\ - Number of removed nodes with same IP and same type = {num_removed_same_ip_same_type},\ - {node_reward_type} quota = {max_rewardable_nodes_same_type}" - )); - } + .ok_or(format!("{LOG_PREFIX}do_add_node: Node Operator does not have rewardable nodes for {node_reward_type}"))? + }; + + let num_in_registry_same_type = get_node_operator_nodes(self, caller_id) + .into_iter() + .filter_map(|node| node.node_reward_type) + .filter(|t| t == &(node_reward_type as i32)) + .count() as u32; + + // Validate node operator's max_rewardable_nodes quota. + let num_remaining_nodes = + num_in_registry_same_type.saturating_sub(num_removed_same_ip_same_type); + if num_remaining_nodes >= max_rewardable_nodes_same_type { + return Err(format!( + "{LOG_PREFIX}do_add_node: Node Operator has reached max_rewardable_nodes quota for {node_reward_type}.\ + Number of nodes in the registry with {node_reward_type} type = {num_in_registry_same_type},\ + Number of removed nodes with same IP and same type = {num_removed_same_ip_same_type},\ + {node_reward_type} quota = {max_rewardable_nodes_same_type}" + )); } } @@ -1162,7 +1167,12 @@ mod tests { registry_add_node_operator_for_node(&mut registry, node_ids[0], btreemap! {}); // Adding several nodes of this type should all succeed even - // though no quota is configured. + // though no quota is configured. Note that for other reward types + // this would fail; the immediately following + // `should_panic_if_max_rewardable_nodes_is_exhausted_for_type4dot5` + // test demonstrates that the underlying quota check is still + // exercised, so passing here is meaningful and not simply due to + // the check being a no-op. for i in 0..3_u8 { let (payload, _) = prepare_add_node_payload(10 + i, node_reward_type); registry diff --git a/rs/registry/canister/unreleased_changelog.md b/rs/registry/canister/unreleased_changelog.md index 92d80ddadccd..3f105ccf91db 100644 --- a/rs/registry/canister/unreleased_changelog.md +++ b/rs/registry/canister/unreleased_changelog.md @@ -25,10 +25,17 @@ on the process that this file is part of, see ## Changed * Temporarily bypass the `max_rewardable_nodes` quota check in `add_node` for - node reward types `type4.1` through `type4.4`, allowing node providers to - register an arbitrary number of such nodes. `type4.5` is explicitly excluded - and still subject to the quota. This is a temporary measure until rewards of - `type4.5` are no longer treated as `type1.1` (see CLO-15). + node reward types `type4.1` through `type4.4` (implemented by treating their + effective quota as unbounded), allowing node providers to register an + arbitrary number of such nodes. `type4.5` is explicitly excluded and remains + subject to the standard quota. + + Motivation: node providers are starting to deploy gen4 hardware now, but the + reward canister currently still treats `type4.5` rewards as `type1.1`, which + means we cannot yet meaningfully size `max_rewardable_nodes` quotas for the + `type4.x` family. Enforcing the quota in the meantime would block legitimate + gen4 onboarding. The quota check will be restored once the reward-side + handling of `type4.5` is fixed (see CLO-15). * One-time post-upgrade migration converting the reward type of 100 currently unassigned nodes from `type1.1` to `type4.5`. The migration only mutates nodes whose reward type is still `type1.1`, so it is idempotent across upgrades. From 480780fc27acb3d6879a29bcc475809cce545ea8 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Thu, 18 Jun 2026 11:56:14 +0000 Subject: [PATCH 7/9] moving env var outside --- .../mutations/node_management/do_add_node.rs | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/rs/registry/canister/src/mutations/node_management/do_add_node.rs b/rs/registry/canister/src/mutations/node_management/do_add_node.rs index 026f2c0b9d6c..10384da06588 100644 --- a/rs/registry/canister/src/mutations/node_management/do_add_node.rs +++ b/rs/registry/canister/src/mutations/node_management/do_add_node.rs @@ -37,6 +37,19 @@ use ic_registry_keys::{NODE_REWARDS_TABLE_KEY, make_replica_version_key}; use ic_types::{crypto::CurrentNodePublicKeys, time::Time}; use prost::Message; +/// Effective per-node-operator cap on the number of `type4.1`-`type4.4` nodes +/// that may be registered, used in lieu of the standard +/// `max_rewardable_nodes`-based quota for those reward types. +/// +/// TODO(CLO-15): Remove this constant and the associated special case in +/// `do_add_node_` once the reward canister no longer treats `type4.5` rewards +/// as `type1.1`. Until then, we cannot meaningfully size +/// `max_rewardable_nodes` for the `type4.x` family without blocking legitimate +/// gen4 onboarding, so we substitute a large-but-bounded sentinel here. The +/// value is chosen to be comfortably above any realistic per-operator +/// deployment while still preventing runaway registrations. +const EXCESSIVE_NUMBER_OF_TYPE_4_NODES: u32 = 1_000; + impl Registry { /// Adds a new node to the registry. /// @@ -136,14 +149,10 @@ impl Registry { "{LOG_PREFIX}do_add_node: Node reward type is required." ))?; - // TODO(CLO-15): Remove the type4.1-type4.4 special case below once - // we no longer treat rewards of type4.5 as type1.1. For now, node - // providers must be able to deploy arbitrarily many nodes of these - // types without being constrained by `max_rewardable_nodes`. Rather - // than branching around the check, we model this as an effectively - // unbounded quota so the same code path is exercised for every - // reward type. Type4.5 is explicitly excluded from this exemption. - const EXCESSIVE_NUMBER_OF_TYPE_4_NODES: u32 = 1_000; + // See `EXCESSIVE_NUMBER_OF_TYPE_4_NODES` at the top of this file for + // why type4.1-type4.4 are handled via a sentinel quota rather than + // the per-operator `max_rewardable_nodes` map. Type4.5 is + // explicitly excluded from this exemption. let is_unbounded_type4 = matches!( node_reward_type, NodeRewardType::Type4dot1 From 74ce9423647ce57a4d99779f3cd6e27612f1e95e Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 19 Jun 2026 00:24:35 +0000 Subject: [PATCH 8/9] suggestions --- .../src/mutations/node_management/do_add_node.rs | 4 ++-- rs/registry/canister/unreleased_changelog.md | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/rs/registry/canister/src/mutations/node_management/do_add_node.rs b/rs/registry/canister/src/mutations/node_management/do_add_node.rs index 10384da06588..55d4117736ca 100644 --- a/rs/registry/canister/src/mutations/node_management/do_add_node.rs +++ b/rs/registry/canister/src/mutations/node_management/do_add_node.rs @@ -153,7 +153,7 @@ impl Registry { // why type4.1-type4.4 are handled via a sentinel quota rather than // the per-operator `max_rewardable_nodes` map. Type4.5 is // explicitly excluded from this exemption. - let is_unbounded_type4 = matches!( + let is_permissionless_type_4_x = matches!( node_reward_type, NodeRewardType::Type4dot1 | NodeRewardType::Type4dot2 @@ -161,7 +161,7 @@ impl Registry { | NodeRewardType::Type4dot4 ); - let max_rewardable_nodes_same_type = if is_unbounded_type4 { + let max_rewardable_nodes_same_type = if is_permissionless_type_4_x { EXCESSIVE_NUMBER_OF_TYPE_4_NODES } else { *node_operator_record diff --git a/rs/registry/canister/unreleased_changelog.md b/rs/registry/canister/unreleased_changelog.md index 3f105ccf91db..051deff26616 100644 --- a/rs/registry/canister/unreleased_changelog.md +++ b/rs/registry/canister/unreleased_changelog.md @@ -24,11 +24,13 @@ on the process that this file is part of, see ## Changed -* Temporarily bypass the `max_rewardable_nodes` quota check in `add_node` for - node reward types `type4.1` through `type4.4` (implemented by treating their - effective quota as unbounded), allowing node providers to register an - arbitrary number of such nodes. `type4.5` is explicitly excluded and remains - subject to the standard quota. +* Temporarily bypass the per-operator `max_rewardable_nodes` quota check in + `add_node` for node reward types `type4.1` through `type4.4`. Instead of the + configured quota, these types are subjected to a single high sentinel cap + (`EXCESSIVE_NUMBER_OF_TYPE_4_NODES`, currently 1000 per node operator), + chosen to be well above any realistic per-operator deployment while still + preventing runaway registrations. `type4.5` is explicitly excluded and + remains subject to the standard `max_rewardable_nodes` quota. Motivation: node providers are starting to deploy gen4 hardware now, but the reward canister currently still treats `type4.5` rewards as `type1.1`, which From 8cc2410442f2512f736adc34c8ff225bdbf9f3a1 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 19 Jun 2026 11:01:43 +0000 Subject: [PATCH 9/9] adding a todo comment --- .../canister/src/mutations/node_management/do_add_node.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rs/registry/canister/src/mutations/node_management/do_add_node.rs b/rs/registry/canister/src/mutations/node_management/do_add_node.rs index 55d4117736ca..0339186907c2 100644 --- a/rs/registry/canister/src/mutations/node_management/do_add_node.rs +++ b/rs/registry/canister/src/mutations/node_management/do_add_node.rs @@ -177,6 +177,10 @@ impl Registry { .count() as u32; // Validate node operator's max_rewardable_nodes quota. + // TODO(@pietrodimarco-dfinity): confirm whether `>=` is intended + // here (i.e. `num_remaining_nodes == max_rewardable_nodes_same_type` + // should reject) or whether this should be `>`. Preserving the + // pre-existing behavior for now. let num_remaining_nodes = num_in_registry_same_type.saturating_sub(num_removed_same_ip_same_type); if num_remaining_nodes >= max_rewardable_nodes_same_type {