Skip to content

Commit 9570db8

Browse files
committed
add sev_enabled subnet invariant
1 parent 8d250c8 commit 9570db8

File tree

4 files changed

+171
-20
lines changed

4 files changed

+171
-20
lines changed

rs/registry/canister/src/common/test_helpers.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,28 @@ pub fn prepare_registry_with_nodes_and_node_operator_id(
112112
start_mutation_id: u8,
113113
nodes: u64,
114114
node_operator_id: PrincipalId,
115+
) -> (RegistryAtomicMutateRequest, BTreeMap<NodeId, PublicKey>) {
116+
prepare_registry_raw(start_mutation_id, nodes, node_operator_id, false)
117+
}
118+
119+
/// Same as above, just with the possibility to have a chip_id.
120+
pub fn prepare_registry_with_nodes_and_chip_id(
121+
start_mutation_id: u8,
122+
nodes: u64,
123+
) -> (RegistryAtomicMutateRequest, BTreeMap<NodeId, PublicKey>) {
124+
prepare_registry_raw(
125+
start_mutation_id,
126+
nodes,
127+
PrincipalId::new_user_test_id(999),
128+
true,
129+
)
130+
}
131+
132+
pub fn prepare_registry_raw(
133+
start_mutation_id: u8,
134+
nodes: u64,
135+
node_operator_id: PrincipalId,
136+
with_chip_id: bool,
115137
) -> (RegistryAtomicMutateRequest, BTreeMap<NodeId, PublicKey>) {
116138
// Prepare a transaction to add the nodes to the registry
117139
let mut mutations = Vec::<RegistryMutation>::default();
@@ -135,6 +157,11 @@ pub fn prepare_registry_with_nodes_and_node_operator_id(
135157
// Preset this field to Some(), in order to allow seamless creation of ApiBoundaryNodeRecord if needed.
136158
domain: Some(format!("node{effective_id}.example.com")),
137159
node_reward_type: Some(NodeRewardType::Type1 as i32),
160+
chip_id: if with_chip_id {
161+
Some(format!("chip-id-{effective_id}").into_bytes())
162+
} else {
163+
None
164+
},
138165
..Default::default()
139166
};
140167
mutations.append(&mut make_add_node_registry_mutations(

rs/registry/canister/src/invariants/subnet.rs

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use std::{
44
};
55

66
use crate::invariants::common::{
7-
InvariantCheckError, RegistrySnapshot, get_subnet_ids_from_snapshot,
7+
InvariantCheckError, RegistrySnapshot, get_node_record_from_snapshot,
8+
get_subnet_ids_from_snapshot,
89
};
910

1011
use ic_base_types::{NodeId, PrincipalId};
@@ -36,6 +37,7 @@ pub(crate) fn check_subnet_invariants(
3637
panic!("Subnet {subnet_id:} is in subnet list but no record exists")
3738
});
3839

40+
// Each SSH key access list does not contain more than 50 keys
3941
if subnet_record.ssh_readonly_access.len() > MAX_NUM_SSH_KEYS
4042
|| subnet_record.ssh_backup_access.len() > MAX_NUM_SSH_KEYS
4143
{
@@ -52,42 +54,43 @@ pub(crate) fn check_subnet_invariants(
5254
});
5355
}
5456

55-
let num_nodes = subnet_record.membership.len();
56-
let mut subnet_members: HashSet<NodeId> = subnet_record
57+
let subnet_members: HashSet<NodeId> = subnet_record
5758
.membership
5859
.iter()
5960
.map(|v| NodeId::from(PrincipalId::try_from(v).unwrap()))
6061
.collect();
6162

6263
// Subnet membership must contain registered nodes only
63-
subnet_members.retain(|&k| {
64-
let node_key = make_node_record_key(k);
65-
let node_exists = snapshot.contains_key(node_key.as_bytes());
66-
if !node_exists {
67-
panic!("Node {k} does not exist in Subnet {subnet_id}");
64+
for &node_id in &subnet_members {
65+
let node_key = make_node_record_key(node_id);
66+
if !snapshot.contains_key(node_key.as_bytes()) {
67+
panic!("Node {node_id} does not exist in Subnet {subnet_id}");
6868
}
69-
node_exists
70-
});
69+
}
7170

7271
// Each node appears at most once in a subnet membership
72+
let num_nodes = subnet_record.membership.len();
7373
if num_nodes > subnet_members.len() {
7474
panic!("Repeated nodes in subnet {subnet_id:}");
7575
}
76+
7677
// Each subnet contains at least one node
7778
if subnet_members.is_empty() {
7879
panic!("No node in subnet {subnet_id:}");
7980
}
81+
82+
// Each node appears at most once in at most one subnet membership
8083
let intersection = accumulated_nodes_in_subnets
8184
.intersection(&subnet_members)
8285
.collect::<HashSet<_>>();
83-
// Each node appears at most once in at most one subnet membership
8486
if !intersection.is_empty() {
8587
return Err(InvariantCheckError {
8688
msg: format!("Nodes in subnet {subnet_id:} also belong to other subnets"),
8789
source: None,
8890
});
8991
}
9092
accumulated_nodes_in_subnets.extend(&subnet_members);
93+
9194
// Count occurrence of system subnets
9295
if subnet_record.subnet_type == i32::from(SubnetType::System) {
9396
system_subnet_count += 1;
@@ -105,6 +108,26 @@ pub(crate) fn check_subnet_invariants(
105108
source: None,
106109
});
107110
}
111+
112+
// SEV-enabled subnets consist of SEV-enabled nodes only (i.e. nodes with a chip ID in the node record)
113+
if let Some(features) = subnet_record.features.as_ref() {
114+
if features.sev_enabled == Some(true) {
115+
for &node_id in &subnet_members {
116+
// handle missing node record
117+
let node_record = get_node_record_from_snapshot(node_id, snapshot)?
118+
.ok_or_else(|| InvariantCheckError {
119+
msg: format!("Subnet {subnet_id} has node {node_id} in its membership but the node record does not exist"),
120+
source: None,
121+
})?;
122+
123+
// handle missing chip_id
124+
node_record.chip_id.as_ref().ok_or_else(|| InvariantCheckError {
125+
msg: format!("Subnet {subnet_id} is SEV-enabled but at least one of its nodes is not: {node_id} does not have a chip ID in its node record"),
126+
source: None,
127+
})?;
128+
}
129+
}
130+
}
108131
}
109132

110133
// There is at least one system subnet. Note that we disable this invariant for benchmarks, as

rs/registry/canister/src/invariants/subnet/tests.rs

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use crate::invariants::common::RegistrySnapshot;
44
use ic_base_types::SubnetId;
55
use ic_protobuf::registry::{
66
node::v1::NodeRecord,
7-
subnet::v1::{CanisterCyclesCostSchedule, SubnetListRecord, SubnetRecord, SubnetType},
7+
subnet::v1::{
8+
CanisterCyclesCostSchedule, SubnetFeatures, SubnetListRecord, SubnetRecord, SubnetType,
9+
},
810
};
911
use ic_registry_keys::{make_subnet_list_record_key, make_subnet_record_key};
1012
use ic_test_utilities_types::ids::{node_test_id, subnet_test_id};
@@ -17,6 +19,8 @@ fn only_application_subnets_can_be_free_cycles_cost_schedule() {
1719
setup_minimal_registry_snapshot_for_check_subnet_invariants(
1820
system_subnet_id,
1921
test_subnet_id,
22+
1,
23+
false,
2024
);
2125

2226
// Trivial case. (Never forget the trivial case, because this is an edge
@@ -54,9 +58,94 @@ fn only_application_subnets_can_be_free_cycles_cost_schedule() {
5458
);
5559
}
5660

61+
#[test]
62+
fn only_sev_enabled_subnets_consist_of_sev_enabled_nodes() {
63+
let system_subnet_id = subnet_test_id(1);
64+
let test_subnet_id = subnet_test_id(2);
65+
let test_node_id = node_test_id(103);
66+
67+
// get a snapshot without chip IDs
68+
let (mut snapshot, mut test_subnet_record) =
69+
setup_minimal_registry_snapshot_for_check_subnet_invariants(
70+
system_subnet_id,
71+
test_subnet_id,
72+
4,
73+
false,
74+
);
75+
76+
// a non SEV-enabled subnet only with nodes without chip ID is compliant
77+
check_subnet_invariants(&snapshot).unwrap();
78+
79+
// a non SEV-enabled subnet with some nodes with and some without chip IDs is compliant
80+
let mut test_node_record = NodeRecord::default();
81+
test_node_record.chip_id = Some("a chip id".into());
82+
snapshot.insert(
83+
make_node_record_key(test_node_id).into_bytes(),
84+
test_node_record.encode_to_vec(),
85+
);
86+
check_subnet_invariants(&snapshot).unwrap();
87+
88+
// an SEV-enabled subnet only with nodes without chip ID is NOT compliant
89+
test_subnet_record.features = Some(SubnetFeatures {
90+
sev_enabled: Some(true),
91+
..Default::default()
92+
});
93+
snapshot.insert(
94+
make_subnet_record_key(test_subnet_id).into_bytes(),
95+
test_subnet_record.encode_to_vec(),
96+
);
97+
let mut test_node_record = NodeRecord::default();
98+
test_node_record.chip_id = None;
99+
snapshot.insert(
100+
make_node_record_key(test_node_id).into_bytes(),
101+
test_node_record.encode_to_vec(),
102+
);
103+
assert_non_compliant_record(
104+
&snapshot,
105+
"subnet fbysm-3acaa-aaaaa-aaaap-yai is sev-enabled but at least one of its nodes is not",
106+
);
107+
108+
// get a snapshot with chip IDs
109+
let (mut snapshot, mut test_subnet_record) =
110+
setup_minimal_registry_snapshot_for_check_subnet_invariants(
111+
system_subnet_id,
112+
test_subnet_id,
113+
4,
114+
true,
115+
);
116+
117+
// a non SEV-enabled subnet only with nodes without chip ID is compliant
118+
check_subnet_invariants(&snapshot).unwrap();
119+
120+
// an SEV-enabled subnet only with nodes with chip ID is compliant
121+
test_subnet_record.features = Some(SubnetFeatures {
122+
sev_enabled: Some(true),
123+
..Default::default()
124+
});
125+
snapshot.insert(
126+
make_subnet_record_key(test_subnet_id).into_bytes(),
127+
test_subnet_record.encode_to_vec(),
128+
);
129+
check_subnet_invariants(&snapshot).unwrap();
130+
131+
// an SEV-enabled subnet with some nodes with and some without chip ID is NOT compliant
132+
let mut test_node_record = NodeRecord::default();
133+
test_node_record.chip_id = None;
134+
snapshot.insert(
135+
make_node_record_key(test_node_id).into_bytes(),
136+
test_node_record.encode_to_vec(),
137+
);
138+
assert_non_compliant_record(
139+
&snapshot,
140+
"subnet fbysm-3acaa-aaaaa-aaaap-yai is sev-enabled but at least one of its nodes is not",
141+
);
142+
}
143+
57144
fn setup_minimal_registry_snapshot_for_check_subnet_invariants(
58145
system_subnet_id: SubnetId,
59146
test_subnet_id: SubnetId,
147+
num_nodes_in_test_subnet: usize,
148+
with_chip_id: bool,
60149
) -> (RegistrySnapshot, SubnetRecord) {
61150
let mut snapshot = RegistrySnapshot::new();
62151

@@ -84,11 +173,18 @@ fn setup_minimal_registry_snapshot_for_check_subnet_invariants(
84173
);
85174

86175
// Add a test subnet in the subnet list.
87-
let test_node_id = node_test_id(100);
88-
snapshot.insert(
89-
make_node_record_key(test_node_id.to_owned()).into_bytes(),
90-
NodeRecord::default().encode_to_vec(),
91-
);
176+
for i in 0..num_nodes_in_test_subnet {
177+
let node_id = node_test_id((i + 100) as u64);
178+
let mut node_record = NodeRecord::default();
179+
if with_chip_id {
180+
node_record.chip_id = Some(format!("chip-id-{i}").into_bytes());
181+
}
182+
snapshot.insert(
183+
make_node_record_key(node_id.to_owned()).into_bytes(),
184+
node_record.encode_to_vec(),
185+
);
186+
}
187+
92188
let subnet_list_record = SubnetListRecord {
93189
subnets: vec![
94190
system_subnet_id.get().into_vec(),
@@ -99,8 +195,12 @@ fn setup_minimal_registry_snapshot_for_check_subnet_invariants(
99195
make_subnet_list_record_key().into_bytes(),
100196
subnet_list_record.encode_to_vec(),
101197
);
198+
let membership = (0..num_nodes_in_test_subnet)
199+
.map(|i| node_test_id((i + 100) as u64).get().to_vec())
200+
.collect();
201+
102202
let test_subnet_record = SubnetRecord {
103-
membership: vec![test_node_id.get().to_vec()],
203+
membership: membership,
104204
..Default::default()
105205
};
106206
snapshot.insert(
@@ -116,5 +216,6 @@ fn assert_non_compliant_record(snapshot: &RegistrySnapshot, error_msg: &str) {
116216
panic!("Expected Err, but got Ok!");
117217
};
118218
let message = err.msg.to_lowercase();
219+
println!("Error message: {message}");
119220
assert!(message.contains(error_msg));
120221
}

rs/registry/canister/src/mutations/do_update_subnet.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ mod tests {
508508
use super::*;
509509
use crate::common::test_helpers::{
510510
add_fake_subnet, get_invariant_compliant_subnet_record, invariant_compliant_registry,
511-
prepare_registry_with_nodes,
511+
prepare_registry_with_nodes, prepare_registry_with_nodes_and_chip_id,
512512
};
513513
use ic_management_canister_types_private::{
514514
EcdsaCurve, EcdsaKeyId, SchnorrAlgorithm, SchnorrKeyId, VetKdCurve, VetKdKeyId,
@@ -979,7 +979,7 @@ mod tests {
979979
fn make_registry_for_update_subnet_tests() -> (Registry, SubnetId) {
980980
let mut registry = invariant_compliant_registry(0);
981981

982-
let (mutate_request, node_ids_and_dkg_pks) = prepare_registry_with_nodes(1, 2);
982+
let (mutate_request, node_ids_and_dkg_pks) = prepare_registry_with_nodes_and_chip_id(1, 2);
983983
registry.maybe_apply_mutation_internal(mutate_request.mutations);
984984

985985
let mut subnet_list_record = registry.get_subnet_list_record();

0 commit comments

Comments
 (0)