use super::*;
use crate::install_root::fleet_registry_recovery::{
ComponentProvisioningSuccessorEvidence, require_active_or_service_successor_evidence,
require_joining_or_recovered_evidence,
};
use candid::Principal;
use canic_core::{
bootstrap::{
compiled::{FleetServiceMemberPurpose, FleetServicePlacementPolicy},
parse_config_model,
},
cdk::types::Cycles,
dto::component_provisioning::{
FleetComponentProvisioningOperation, FleetComponentProvisioningPhase,
},
dto::fleet_registry::{
FleetServiceBinding, FleetServiceComponentBinding, FleetServiceMode, FleetSubnetRootEntry,
FleetSubnetRootStatus,
},
ids::{
AppId, CanonicalNetworkId, ComponentGroupMemberPath, ComponentGroupPlacementId,
ComponentInstanceId, ComponentSpecAdmission, CyclesFundingBudget, FleetBinding,
FleetCoordinatorBinding, FleetId, FleetKey, FleetRegistryAuthority,
FleetSubnetCanisterPoolConfig, FleetSubnetRootLimits, FleetSubnetRootReleaseSet,
ReleaseBuildId, ReleaseBuildNonce, ReleaseSetDigest, SubnetId,
},
shared_support::fleet_admission_policy::compile_installed_fleet_admission_policy,
};
#[test]
fn verified_activation_accepts_only_the_exact_initial_service_successor() {
let topology = topology();
let active = active_registry(&topology);
let successor = FleetRegistryOps::compile_initial_services(
&active.authority,
&topology,
&active,
vec![service()],
)
.expect("compile service successor");
let exact_live = live_evidence(&topology, successor.clone());
let expected_operation_id = [21; 32];
let expected_plan_hash = [22; 32];
require_active_or_service_successor_evidence(
&topology,
&active,
&live_evidence(&topology, active.clone()),
expected_operation_id,
expected_plan_hash,
None,
)
.expect("accept the exact retained all-Active Registry without successor evidence");
let successor_evidence = ComponentProvisioningSuccessorEvidence {
operation_id: expected_operation_id,
plan_hash: expected_plan_hash,
source_registry: FleetRegistryOps::version(&active.authority, &topology, &active)
.expect("active Registry version"),
published_registry: Some(exact_live.version.clone()),
operation: FleetComponentProvisioningOperation::FreshInstall,
phase: FleetComponentProvisioningPhase::ConfirmingDirectories,
};
require_active_or_service_successor_evidence(
&topology,
&active,
&exact_live,
expected_operation_id,
expected_plan_hash,
Some(&successor_evidence),
)
.expect("accept exact service successor");
assert!(
require_active_or_service_successor_evidence(
&topology,
&active,
&exact_live,
expected_operation_id,
expected_plan_hash,
None,
)
.is_err(),
"a service successor without exact Coordinator provisioning evidence must fail closed"
);
let mut wrong_plan = successor_evidence.clone();
wrong_plan.plan_hash[0] ^= 1;
assert!(
require_active_or_service_successor_evidence(
&topology,
&active,
&exact_live,
expected_operation_id,
expected_plan_hash,
Some(&wrong_plan),
)
.is_err(),
"a service successor without the exact durable Component plan must fail closed"
);
let mut premature = successor_evidence.clone();
premature.phase = FleetComponentProvisioningPhase::ComponentsProvisioned;
assert!(
require_active_or_service_successor_evidence(
&topology,
&active,
&exact_live,
expected_operation_id,
expected_plan_hash,
Some(&premature),
)
.is_err(),
"a successor must not be accepted before the Coordinator publishes its services"
);
let mut later = successor;
later.revision += 1;
let later_live = live_evidence(&topology, later);
assert!(
require_active_or_service_successor_evidence(
&topology,
&active,
&later_live,
expected_operation_id,
expected_plan_hash,
Some(&successor_evidence),
)
.is_err(),
"a later canonical Registry must not masquerade as the one service successor"
);
}
#[test]
fn initial_service_successor_rejects_changed_admission_authority() {
let topology = topology();
let active = active_registry(&topology);
let mut successor = FleetRegistryOps::compile_initial_services(
&active.authority,
&topology,
&active,
vec![service()],
)
.expect("compile service successor");
successor.admission = compile_installed_fleet_admission_policy(
successor.admission.fleet.clone(),
successor.admission.generation + 1,
successor.admission.fleet_principals.clone(),
successor.admission.rules.clone(),
)
.expect("valid divergent admission authority");
let live = live_evidence(&topology, successor);
let operation_id = [24; 32];
let plan_hash = [25; 32];
let evidence = ComponentProvisioningSuccessorEvidence {
operation_id,
plan_hash,
source_registry: FleetRegistryOps::version(&active.authority, &topology, &active)
.expect("active Registry version"),
published_registry: Some(live.version.clone()),
operation: FleetComponentProvisioningOperation::FreshInstall,
phase: FleetComponentProvisioningPhase::ServiceTopologyPublished,
};
assert!(
require_active_or_service_successor_evidence(
&topology,
&active,
&live,
operation_id,
plan_hash,
Some(&evidence),
)
.is_err(),
"an initial-service successor must not change admission authority"
);
}
#[test]
fn join_recovery_reaches_the_exact_initial_service_successor() {
let topology = topology();
let authority = authority();
let genesis = FleetRegistryOps::compile_genesis(
&AppId::from("demo"),
authority.clone(),
&topology,
crate::test_support::fleet_admission_policy(authority.binding.fleet.clone()),
)
.expect("genesis Registry");
let joining =
FleetRegistryOps::compile_joining(&authority, &topology, &genesis, root(&topology))
.expect("Joining Registry");
let active = FleetRegistryOps::compile_active(&authority, &topology, &joining)
.expect("all-Active Registry");
let successor =
FleetRegistryOps::compile_initial_services(&authority, &topology, &active, vec![service()])
.expect("initial service successor");
let successor_live = live_evidence(&topology, successor);
let operation_id = [31; 32];
let plan_hash = [32; 32];
let evidence = ComponentProvisioningSuccessorEvidence {
operation_id,
plan_hash,
source_registry: FleetRegistryOps::version(&authority, &topology, &active)
.expect("active Registry version"),
published_registry: Some(successor_live.version.clone()),
operation: FleetComponentProvisioningOperation::FreshInstall,
phase: FleetComponentProvisioningPhase::ConfirmingDirectories,
};
require_joining_or_recovered_evidence(
&topology,
&joining,
&active,
&live_evidence(&topology, joining.clone()),
operation_id,
plan_hash,
None,
)
.expect("accept exact all-Joining replay");
require_joining_or_recovered_evidence(
&topology,
&joining,
&active,
&live_evidence(&topology, active.clone()),
operation_id,
plan_hash,
None,
)
.expect("accept exact all-Active replay");
require_joining_or_recovered_evidence(
&topology,
&joining,
&active,
&successor_live,
operation_id,
plan_hash,
Some(&evidence),
)
.expect("accept exact service successor at the join recovery boundary");
}
fn topology() -> ComponentTopology {
parse_config_model(
r#"
[app]
name = "demo"
[roles.root]
kind = "root"
package = "root"
[roles.worker]
kind = "canister"
package = "worker"
[component_specs.worker]
component_role = "worker"
maximum_instances = 1
"#,
)
.expect("valid config")
.compile_component_topology()
.expect("Component Topology")
}
fn active_registry(topology: &ComponentTopology) -> FleetRegistry {
let authority = authority();
let mut registry = FleetRegistryOps::compile_genesis(
&AppId::from("demo"),
authority.clone(),
topology,
crate::test_support::fleet_admission_policy(authority.binding.fleet.clone()),
)
.expect("genesis Registry");
registry = FleetRegistryOps::compile_joining(&authority, topology, ®istry, root(topology))
.expect("Joining root");
FleetRegistryOps::compile_active(&authority, topology, ®istry).expect("active Registry")
}
fn root(topology: &ComponentTopology) -> FleetSubnetRootEntry {
let component_spec = "worker".parse().expect("Component Spec ID");
let spec = topology.get(&component_spec).expect("Component Spec");
let component_admissions = vec![ComponentSpecAdmission {
component_spec,
spec_hash: spec.spec_hash,
maximum_root_instances: 1,
}];
FleetSubnetRootEntry {
placement_subnet: subnet(5),
fleet_subnet_root: principal(6),
component_topology_digest: topology
.project_for_admissions(&component_admissions)
.expect("root projection")
.digest()
.expect("root topology digest"),
component_admissions,
active_release_set: FleetSubnetRootReleaseSet {
release_build_id: ReleaseBuildId::from_nonce(ReleaseBuildNonce::from_random_bytes(
[7; 32],
)),
manifest_digest: ReleaseSetDigest::from_bytes([8; 32]),
},
limits: root_limits(),
funding: crate::test_support::fleet_subnet_root_funding_authority(),
status: FleetSubnetRootStatus::Joining,
}
}
fn service() -> FleetServiceBinding {
FleetServiceBinding {
service: "workers".parse().expect("service ID"),
role: "worker".parse().expect("role"),
component_spec: "worker".parse().expect("Component Spec ID"),
mode: FleetServiceMode::ActivePool,
placement: FleetServicePlacementPolicy {
maximum_members_per_root: 1,
minimum_distinct_roots: 1,
},
members: vec![FleetServiceComponentBinding {
member_purpose: FleetServiceMemberPurpose::PoolMember,
component: ComponentInstanceId::from_generated_bytes([9; 32]),
fleet_subnet_root: principal(6),
canister_id: principal(10),
group_placement: ComponentGroupPlacementId {
deployment: "workers".parse().expect("deployment ID"),
ordinal: 0,
},
member_path: ComponentGroupMemberPath::try_from(vec![
"worker".parse().expect("member ID"),
])
.expect("member path"),
}],
}
}
fn live_evidence(topology: &ComponentTopology, registry: FleetRegistry) -> LiveRegistryEvidence {
LiveRegistryEvidence {
manifest: FleetRegistryOps::manifest(®istry.authority, topology, ®istry)
.expect("Registry manifest"),
version: FleetRegistryOps::version(®istry.authority, topology, ®istry)
.expect("Registry version"),
registry,
}
}
fn authority() -> FleetRegistryAuthority {
FleetRegistryAuthority {
binding: FleetCoordinatorBinding {
fleet: FleetBinding {
fleet: FleetKey {
canonical_network_id: CanonicalNetworkId::ic_mainnet(),
fleet_id: FleetId::from_generated_bytes([1; 32]),
},
app: AppId::from("demo"),
},
coordinator_subnet: subnet(2),
coordinator: principal(3),
},
epoch: 1,
}
}
fn root_limits() -> FleetSubnetRootLimits {
FleetSubnetRootLimits {
maximum_component_instances: 1,
maximum_registry_bytes: 2_097_152,
maximum_wasm_store_bytes: 40_000_000,
maximum_group_placements: 1,
canister_pool: FleetSubnetCanisterPoolConfig {
minimum_size: 1,
maximum_size: 1,
canister_cycles: Cycles::new(5_000_000_000_000),
},
cycles_funding: CyclesFundingBudget {
window_secs: 3_600,
maximum_cycles: Cycles::new(10_000_000_000_000),
},
}
}
fn subnet(byte: u8) -> SubnetId {
SubnetId::from_principal(principal(byte))
}
fn principal(byte: u8) -> Principal {
Principal::from_slice(&[byte; 29])
}