pub mod endpoint_mode;
use crate::{
config::ComponentTopology,
ids::{AppId, FleetBinding, FleetSubnetRootBinding, FleetSubnetRootReleaseSet, ReleaseBuildId},
};
use candid::Principal;
use thiserror::Error as ThisError;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PreparedFleetActivation {
pub identity: FleetActivationIdentity,
pub root_authority: Option<PreparedFleetSubnetRootAuthority>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PreparedFleetSubnetRootAuthority {
pub binding: FleetSubnetRootBinding,
pub initial_release_set: FleetSubnetRootReleaseSet,
pub expected_module_hash: [u8; 32],
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FleetActivationIdentity {
pub fleet: FleetBinding,
pub operation_id: [u8; 32],
pub release_build_id: ReleaseBuildId,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RootInstallIdentity {
pub binding: FleetSubnetRootBinding,
pub initial_release_set: FleetSubnetRootReleaseSet,
pub install_id: [u8; 32],
pub expected_module_hash: [u8; 32],
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NonrootInstallIdentity {
pub fleet: FleetBinding,
pub install_id: [u8; 32],
pub release_build_id: ReleaseBuildId,
}
#[derive(Debug, Eq, PartialEq, ThisError)]
pub enum PrepareFleetActivationError {
#[error(
"install release-build identity {supplied} does not match embedded Wasm identity {embedded}"
)]
ReleaseBuildMismatch {
supplied: ReleaseBuildId,
embedded: ReleaseBuildId,
},
#[error("Fleet Subnet Root authority uses epoch {observed}, expected fresh epoch one")]
AuthorityEpoch { observed: u64 },
#[error(
"Fleet Subnet Root authority App '{authority}' does not match configured App '{configured}'"
)]
AppMismatch { configured: AppId, authority: AppId },
#[error("Fleet Subnet Root binding principal {bound} does not match this Canister {observed}")]
RootPrincipalMismatch {
bound: Principal,
observed: Principal,
},
#[error(transparent)]
Topology(#[from] crate::config::ComponentTopologyError),
}
pub fn prepare_root_install(
input: RootInstallIdentity,
embedded_release_build_id: ReleaseBuildId,
configured_app: &AppId,
component_topology: &ComponentTopology,
root_canister: Principal,
) -> Result<PreparedFleetActivation, PrepareFleetActivationError> {
require_release_build_match(
input.initial_release_set.release_build_id,
embedded_release_build_id,
)?;
if input.binding.authority.epoch != 1 {
return Err(PrepareFleetActivationError::AuthorityEpoch {
observed: input.binding.authority.epoch,
});
}
if &input.binding.authority.binding.fleet.app != configured_app {
return Err(PrepareFleetActivationError::AppMismatch {
configured: configured_app.clone(),
authority: input.binding.authority.binding.fleet.app,
});
}
if input.binding.fleet_subnet_root != root_canister {
return Err(PrepareFleetActivationError::RootPrincipalMismatch {
bound: input.binding.fleet_subnet_root,
observed: root_canister,
});
}
component_topology.validate_root_binding(&input.binding)?;
Ok(PreparedFleetActivation {
identity: FleetActivationIdentity {
fleet: input.binding.authority.binding.fleet.clone(),
operation_id: input.install_id,
release_build_id: input.initial_release_set.release_build_id,
},
root_authority: Some(PreparedFleetSubnetRootAuthority {
binding: input.binding,
initial_release_set: input.initial_release_set,
expected_module_hash: input.expected_module_hash,
}),
})
}
pub fn prepare_nonroot_install(
input: NonrootInstallIdentity,
embedded_release_build_id: ReleaseBuildId,
) -> Result<PreparedFleetActivation, PrepareFleetActivationError> {
require_release_build_match(input.release_build_id, embedded_release_build_id)?;
Ok(PreparedFleetActivation {
identity: FleetActivationIdentity {
fleet: input.fleet,
operation_id: input.install_id,
release_build_id: input.release_build_id,
},
root_authority: None,
})
}
fn require_release_build_match(
supplied: ReleaseBuildId,
embedded: ReleaseBuildId,
) -> Result<(), PrepareFleetActivationError> {
if supplied != embedded {
return Err(PrepareFleetActivationError::ReleaseBuildMismatch { supplied, embedded });
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
cdk::types::Cycles,
config::{ComponentLimits, ComponentSpec},
ids::{
CanisterRole, CanonicalNetworkId, ComponentSpecAdmission, ComponentSpecId,
ComponentTopologyDigest, CyclesFundingBudget, FleetCoordinatorBinding, FleetId,
FleetKey, FleetRegistryAuthority, FleetSubnetRootLimits, ReleaseBuildNonce,
ReleaseSetDigest, SubnetId,
},
};
fn release_build(byte: u8) -> ReleaseBuildId {
ReleaseBuildId::from_nonce(ReleaseBuildNonce::from_random_bytes([byte; 32]))
}
fn input(release_build_id: ReleaseBuildId) -> RootInstallIdentity {
let component_spec: ComponentSpecId = "projects".parse().expect("Component Spec");
RootInstallIdentity {
binding: FleetSubnetRootBinding {
authority: FleetRegistryAuthority {
binding: FleetCoordinatorBinding {
fleet: FleetBinding {
fleet: FleetKey {
canonical_network_id: CanonicalNetworkId::public_ic(),
fleet_id: FleetId::from_generated_bytes([2; 32]),
},
app: AppId::from("toko"),
},
coordinator_subnet: SubnetId::from_principal(Principal::from_slice(
&[4; 29],
)),
coordinator: Principal::from_slice(&[5; 29]),
},
epoch: 1,
},
placement_subnet: SubnetId::from_principal(Principal::from_slice(&[6; 29])),
fleet_subnet_root: Principal::from_slice(&[7; 29]),
component_admissions: vec![ComponentSpecAdmission {
component_spec,
spec_hash: [8; 32],
maximum_root_instances: 2,
}],
component_topology_digest: ComponentTopologyDigest::from_bytes([9; 32]),
limits: FleetSubnetRootLimits {
maximum_component_instances: 10,
maximum_managed_canisters: 1_000,
maximum_registry_bytes: 4_194_304,
maximum_wasm_store_bytes: 40_000_000,
cycles_funding: funding(),
},
},
initial_release_set: FleetSubnetRootReleaseSet {
release_build_id,
manifest_digest: ReleaseSetDigest::from_bytes([10; 32]),
},
install_id: [3; 32],
expected_module_hash: [11; 32],
}
}
fn nonroot_input(release_build_id: ReleaseBuildId) -> NonrootInstallIdentity {
let root = input(release_build_id);
NonrootInstallIdentity {
fleet: root.binding.authority.binding.fleet,
install_id: root.install_id,
release_build_id: root.initial_release_set.release_build_id,
}
}
fn funding() -> CyclesFundingBudget {
CyclesFundingBudget {
window_secs: 3_600,
maximum_cycles: Cycles::new(1_000_000_000_000),
}
}
fn topology() -> ComponentTopology {
ComponentTopology {
component_specs: vec![ComponentSpec {
component_spec: "projects".parse().expect("Component Spec"),
spec_hash: [8; 32],
component_role: CanisterRole::from("project_hub"),
maximum_fleet_instances: 10,
limits: ComponentLimits {
maximum_descendants: 100,
maximum_registry_bytes: 1_048_576,
cycles_funding: funding(),
},
children: Vec::new(),
spawn_grants: Vec::new(),
}],
provisioning_grants: Vec::new(),
}
}
fn prepare_root(
input: RootInstallIdentity,
embedded: ReleaseBuildId,
) -> Result<PreparedFleetActivation, PrepareFleetActivationError> {
let root_canister = input.binding.fleet_subnet_root;
let mut input = input;
input.binding.component_topology_digest = topology()
.project_for_admissions(&input.binding.component_admissions)
.expect("projection")
.digest()
.expect("digest");
prepare_root_install(
input,
embedded,
&AppId::from("toko"),
&topology(),
root_canister,
)
}
#[test]
fn root_install_normalizes_install_identity_only_after_build_match() {
let release_build_id = release_build(5);
let prepared = prepare_root(input(release_build_id), release_build_id).expect("prepare");
assert_eq!(prepared.identity.operation_id, [3; 32]);
assert_eq!(prepared.identity.release_build_id, release_build_id);
}
#[test]
fn root_install_rejects_release_build_mismatch() {
let supplied = release_build(6);
let embedded = release_build(7);
assert_eq!(
prepare_root(input(supplied), embedded),
Err(PrepareFleetActivationError::ReleaseBuildMismatch { supplied, embedded })
);
}
#[test]
fn root_install_rejects_wrong_epoch_app_and_root_principal() {
let release_build_id = release_build(11);
let mut wrong_epoch = input(release_build_id);
wrong_epoch.binding.authority.epoch = 2;
assert_eq!(
prepare_root(wrong_epoch, release_build_id),
Err(PrepareFleetActivationError::AuthorityEpoch { observed: 2 })
);
let mut wrong_app = input(release_build_id);
wrong_app.binding.authority.binding.fleet.app = AppId::from("other");
assert_eq!(
prepare_root(wrong_app, release_build_id),
Err(PrepareFleetActivationError::AppMismatch {
configured: AppId::from("toko"),
authority: AppId::from("other"),
})
);
let mut wrong_root = input(release_build_id);
wrong_root.binding.component_topology_digest = topology()
.project_for_admissions(&wrong_root.binding.component_admissions)
.expect("projection")
.digest()
.expect("digest");
let bound = wrong_root.binding.fleet_subnet_root;
let observed = Principal::from_slice(&[99; 29]);
assert_eq!(
prepare_root_install(
wrong_root,
release_build_id,
&AppId::from("toko"),
&topology(),
observed,
),
Err(PrepareFleetActivationError::RootPrincipalMismatch { bound, observed })
);
}
#[test]
fn nonroot_install_normalizes_the_same_exact_identity() {
let release_build_id = release_build(8);
let prepared = prepare_nonroot_install(nonroot_input(release_build_id), release_build_id)
.expect("prepare");
assert_eq!(prepared.identity.operation_id, [3; 32]);
assert_eq!(prepared.identity.release_build_id, release_build_id);
}
#[test]
fn nonroot_install_rejects_release_build_mismatch() {
let supplied = release_build(9);
let embedded = release_build(10);
assert_eq!(
prepare_nonroot_install(nonroot_input(supplied), embedded),
Err(PrepareFleetActivationError::ReleaseBuildMismatch { supplied, embedded })
);
}
}