Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion rs/registry/canister/src/invariants/subnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::invariants::common::{

use ic_base_types::{NodeId, PrincipalId};
use ic_nns_common::registry::MAX_NUM_SSH_KEYS;
use ic_protobuf::registry::subnet::v1::{SubnetRecord, SubnetType};
use ic_protobuf::registry::subnet::v1::{CanisterCyclesCostSchedule, SubnetRecord, SubnetType};
use ic_registry_keys::{SUBNET_RECORD_KEY_PREFIX, make_node_record_key, make_subnet_record_key};
use prost::Message;

Expand All @@ -20,6 +20,7 @@ use prost::Message;
/// * Each subnet contains at least one node
/// * There is at least one system subnet
/// * Each subnet in the registry occurs in the subnet list and vice versa
/// * Only application subnets can be rented and therefore have a "free" cycles cost schedule
pub(crate) fn check_subnet_invariants(
snapshot: &RegistrySnapshot,
) -> Result<(), InvariantCheckError> {
Expand Down Expand Up @@ -91,7 +92,21 @@ pub(crate) fn check_subnet_invariants(
if subnet_record.subnet_type == i32::from(SubnetType::System) {
system_subnet_count += 1;
}

// Only application subnets can be rented and have a "free" cycles cost schedule.
if subnet_record.subnet_type != i32::from(SubnetType::Application)
&& subnet_record.canister_cycles_cost_schedule
== i32::from(CanisterCyclesCostSchedule::Free)
{
return Err(InvariantCheckError {
msg: format!(
"Subnet {subnet_id:} is not an application subnet but has a free cycles cost schedule"
),
source: None,
});
}
}

// There is at least one system subnet. Note that we disable this invariant for benchmarks, as
// the code to set up "invariants compliant" registry mostly depends on "test-only" code, and
// it's very difficult to conform canbench benchmarks to test-only code. It's also risky to move
Expand Down Expand Up @@ -129,3 +144,6 @@ pub(crate) fn get_subnet_records_map(
}
subnets
}

#[cfg(test)]
mod tests;
120 changes: 120 additions & 0 deletions rs/registry/canister/src/invariants/subnet/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use super::*;

use crate::invariants::common::RegistrySnapshot;
use ic_base_types::SubnetId;
use ic_protobuf::registry::{
node::v1::NodeRecord,
subnet::v1::{CanisterCyclesCostSchedule, SubnetListRecord, SubnetRecord, SubnetType},
};
use ic_registry_keys::{make_subnet_list_record_key, make_subnet_record_key};
use ic_test_utilities_types::ids::{node_test_id, subnet_test_id};

#[test]
fn only_application_subnets_can_be_free_cycles_cost_schedule() {
let system_subnet_id = subnet_test_id(1);
let test_subnet_id = subnet_test_id(2);
let (mut snapshot, mut test_subnet_record) =
setup_minimal_registry_snapshot_for_check_subnet_invariants(
system_subnet_id,
test_subnet_id,
);

// Trivial case. (Never forget the trivial case, because this is an edge
// case, and edge cases is where many mistakes are made.)
check_subnet_invariants(&snapshot).unwrap();

// Happy case: a compliant `SubnetRecord`.
test_subnet_record.subnet_type = i32::from(SubnetType::Application);
test_subnet_record.canister_cycles_cost_schedule = i32::from(CanisterCyclesCostSchedule::Free);
snapshot.insert(
make_subnet_record_key(test_subnet_id).into_bytes(),
test_subnet_record.encode_to_vec(),
);
check_subnet_invariants(&snapshot).unwrap();

// System or verified application subnets cannot be on "free" cycles cost schedule.
test_subnet_record.subnet_type = i32::from(SubnetType::System);
snapshot.insert(
make_subnet_record_key(test_subnet_id).into_bytes(),
test_subnet_record.encode_to_vec(),
);
assert_non_compliant_record(
&snapshot,
"is not an application subnet but has a free cycles cost schedule",
);

test_subnet_record.subnet_type = i32::from(SubnetType::VerifiedApplication);
snapshot.insert(
make_subnet_record_key(test_subnet_id).into_bytes(),
test_subnet_record.encode_to_vec(),
);
assert_non_compliant_record(
&snapshot,
"is not an application subnet but has a free cycles cost schedule",
);
}

fn setup_minimal_registry_snapshot_for_check_subnet_invariants(
system_subnet_id: SubnetId,
test_subnet_id: SubnetId,
) -> (RegistrySnapshot, SubnetRecord) {
let mut snapshot = RegistrySnapshot::new();

let system_node_id = node_test_id(1);
snapshot.insert(
make_node_record_key(system_node_id.to_owned()).into_bytes(),
NodeRecord::default().encode_to_vec(),
);

let subnet_list_record = SubnetListRecord {
subnets: vec![system_subnet_id.get().into_vec()],
};
snapshot.insert(
make_subnet_list_record_key().into_bytes(),
subnet_list_record.encode_to_vec(),
);
let subnet_record = SubnetRecord {
membership: vec![system_node_id.get().into_vec()],
subnet_type: i32::from(SubnetType::System),
..Default::default()
};
snapshot.insert(
make_subnet_record_key(system_subnet_id).into_bytes(),
subnet_record.encode_to_vec(),
);

// Add a test subnet in the subnet list.
let test_node_id = node_test_id(100);
snapshot.insert(
make_node_record_key(test_node_id.to_owned()).into_bytes(),
NodeRecord::default().encode_to_vec(),
);
let subnet_list_record = SubnetListRecord {
subnets: vec![
system_subnet_id.get().into_vec(),
test_subnet_id.get().into_vec(),
],
};
snapshot.insert(
make_subnet_list_record_key().into_bytes(),
subnet_list_record.encode_to_vec(),
);
let test_subnet_record = SubnetRecord {
membership: vec![test_node_id.get().to_vec()],
..Default::default()
};
snapshot.insert(
make_subnet_record_key(test_subnet_id).into_bytes(),
test_subnet_record.encode_to_vec(),
);

(snapshot, test_subnet_record)
}

fn assert_non_compliant_record(snapshot: &RegistrySnapshot, error_msg: &str) {
let Err(err) = check_subnet_invariants(snapshot) else {
panic!("Expected Err, but got Ok!");
};
let message = err.msg.to_lowercase();
assert!(message.contains(error_msg));
}