canic-core 0.42.11

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
use super::registry::MemoryRegistryError;
use ic_memory::{
    AllocationPolicy, AllocationSlotDescriptor, MemoryManagerAuthorityRecord, MemoryManagerIdRange,
    MemoryManagerRangeMode, MemoryManagerSlotError, StableKey,
};

pub const CANIC_CORE_MIN_ID: u8 = 11;
pub const CANIC_CORE_MAX_ID: u8 = 79;
pub const CANIC_CONTROL_PLANE_MIN_ID: u8 = 80;
pub const CANIC_CONTROL_PLANE_MAX_ID: u8 = 85;

pub const CANIC_CORE_AUTHORITY_OWNER: &str = "canic-core";
pub const CANIC_CORE_AUTHORITY_PURPOSE: &str = "Canic core allocation authority";
pub const CANIC_CONTROL_PLANE_AUTHORITY_OWNER: &str = "canic-control-plane";
pub const CANIC_CONTROL_PLANE_AUTHORITY_PURPOSE: &str = "Canic control-plane allocation authority";

///
/// CanicMemoryManagerPolicy
///
/// Canic policy adapter for the `ic-memory` MemoryManager substrate
/// allocation slots.

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct CanicMemoryManagerPolicy;

impl CanicMemoryManagerPolicy {
    #[must_use]
    pub(super) const fn new() -> Self {
        Self
    }
}

impl AllocationPolicy for CanicMemoryManagerPolicy {
    type Error = MemoryRegistryError;

    fn validate_key(&self, _key: &StableKey) -> Result<(), Self::Error> {
        Ok(())
    }

    fn validate_slot(
        &self,
        key: &StableKey,
        slot: &AllocationSlotDescriptor,
    ) -> Result<(), Self::Error> {
        let id = slot
            .memory_manager_id()
            .map_err(memory_slot_error_to_registry_error)?;
        validate_key_id_claim(id, key.as_str())
    }

    fn validate_reserved_slot(
        &self,
        key: &StableKey,
        slot: &AllocationSlotDescriptor,
    ) -> Result<(), Self::Error> {
        let id = slot
            .memory_manager_id()
            .map_err(memory_slot_error_to_registry_error)?;
        if !ic_memory::is_ic_memory_stable_key(key.as_str()) && !key.as_str().starts_with("canic.")
        {
            return Err(MemoryRegistryError::RangeAuthorityViolation {
                stable_key: key.as_str().to_string(),
                id,
                reason: "application stable keys may not be pre-reserved by Canic",
            });
        }
        validate_key_id_claim(id, key.as_str())
    }
}

#[must_use]
pub fn canonical_authority_records() -> Vec<MemoryManagerAuthorityRecord> {
    vec![
        MemoryManagerAuthorityRecord::new(
            ic_memory::memory_manager_governance_range(),
            ic_memory::IC_MEMORY_AUTHORITY_OWNER,
            MemoryManagerRangeMode::Reserved,
            Some(ic_memory::IC_MEMORY_AUTHORITY_PURPOSE.to_string()),
        )
        .expect("valid ic-memory authority record"),
        MemoryManagerAuthorityRecord::new(
            canic_core_range(),
            CANIC_CORE_AUTHORITY_OWNER,
            MemoryManagerRangeMode::Reserved,
            Some(CANIC_CORE_AUTHORITY_PURPOSE.to_string()),
        )
        .expect("valid Canic core authority record"),
        MemoryManagerAuthorityRecord::new(
            canic_control_plane_range(),
            CANIC_CONTROL_PLANE_AUTHORITY_OWNER,
            MemoryManagerRangeMode::Reserved,
            Some(CANIC_CONTROL_PLANE_AUTHORITY_PURPOSE.to_string()),
        )
        .expect("valid Canic control-plane authority record"),
    ]
}

fn validate_key_id_claim(id: u8, stable_key: &str) -> Result<(), MemoryRegistryError> {
    if ic_memory::is_ic_memory_stable_key(stable_key) {
        return Ok(());
    }

    if stable_key.starts_with("canic.core.") {
        return require_range(
            id,
            stable_key,
            canic_core_range(),
            "canic.core.* keys must use Canic core ids 11-79",
        );
    }

    if stable_key.starts_with("canic.control_plane.") {
        return require_range(
            id,
            stable_key,
            canic_control_plane_range(),
            "canic.control_plane.* keys must use Canic control-plane ids 80-85",
        );
    }

    if stable_key.starts_with("canic.") {
        return Err(MemoryRegistryError::RangeAuthorityViolation {
            stable_key: stable_key.to_string(),
            id,
            reason: "unrecognized canic.* stable key namespace",
        });
    }

    validate_application_claim(id, stable_key)
}

fn validate_application_claim(id: u8, stable_key: &str) -> Result<(), MemoryRegistryError> {
    if ic_memory::memory_manager_governance_range().contains(id)
        || canic_core_range().contains(id)
        || canic_control_plane_range().contains(id)
    {
        return Err(MemoryRegistryError::RangeAuthorityViolation {
            stable_key: stable_key.to_string(),
            id,
            reason: "application keys may not use reserved MemoryManager IDs",
        });
    }
    Ok(())
}

fn require_range(
    id: u8,
    stable_key: &str,
    range: MemoryManagerIdRange,
    reason: &'static str,
) -> Result<(), MemoryRegistryError> {
    if range.contains(id) {
        Ok(())
    } else {
        Err(MemoryRegistryError::RangeAuthorityViolation {
            stable_key: stable_key.to_string(),
            id,
            reason,
        })
    }
}

fn canic_core_range() -> MemoryManagerIdRange {
    MemoryManagerIdRange::new(CANIC_CORE_MIN_ID, CANIC_CORE_MAX_ID).expect("valid Canic core range")
}

fn canic_control_plane_range() -> MemoryManagerIdRange {
    MemoryManagerIdRange::new(CANIC_CONTROL_PLANE_MIN_ID, CANIC_CONTROL_PLANE_MAX_ID)
        .expect("valid Canic control-plane range")
}

fn memory_slot_error_to_registry_error(err: MemoryManagerSlotError) -> MemoryRegistryError {
    match err {
        MemoryManagerSlotError::InvalidMemoryManagerId { id } => {
            MemoryRegistryError::InvalidDeclaration {
                stable_key: "<slot>".to_string(),
                reason: if id == ic_memory::MEMORY_MANAGER_INVALID_ID {
                    "MemoryManager ID 255 is not usable"
                } else {
                    "MemoryManager ID is not usable"
                },
            }
        }
        MemoryManagerSlotError::UnsupportedSubstrate { .. }
        | MemoryManagerSlotError::UnsupportedDescriptorVersion { .. } => {
            MemoryRegistryError::LedgerCorrupt {
                reason: "unsupported MemoryManager allocation slot descriptor",
            }
        }
    }
}

///
/// TESTS
///

#[cfg(test)]
mod tests {
    use super::*;

    fn policy() -> CanicMemoryManagerPolicy {
        CanicMemoryManagerPolicy::new()
    }

    fn key(value: &str) -> StableKey {
        StableKey::parse(value).expect("stable key")
    }

    fn slot(id: u8) -> AllocationSlotDescriptor {
        AllocationSlotDescriptor::memory_manager(id).expect("usable MemoryManager id")
    }

    #[test]
    fn rejects_memory_manager_sentinel_id_through_ic_memory() {
        let err = AllocationSlotDescriptor::memory_manager(ic_memory::MEMORY_MANAGER_INVALID_ID)
            .expect_err("ID 255 is the unallocated-bucket sentinel");
        assert!(matches!(
            err,
            MemoryManagerSlotError::InvalidMemoryManagerId { id }
                if id == ic_memory::MEMORY_MANAGER_INVALID_ID
        ));
    }

    fn validate(stable_key: &str, id: u8) -> Result<(), MemoryRegistryError> {
        policy().validate_slot(&key(stable_key), &slot(id))
    }

    fn validate_reserved(stable_key: &str, id: u8) -> Result<(), MemoryRegistryError> {
        policy().validate_reserved_slot(&key(stable_key), &slot(id))
    }

    #[test]
    fn accepts_canic_framework_namespaces_in_owned_ranges() {
        validate("canic.core.canister_children.v1", CANIC_CORE_MIN_ID).expect("first core slot");
        validate("canic.core.future.v1", CANIC_CORE_MAX_ID).expect("last core slot");
        validate(
            "canic.control_plane.template_manifest.v1",
            CANIC_CONTROL_PLANE_MIN_ID,
        )
        .expect("first control-plane slot");
        validate(
            "canic.control_plane.wasm_store_gc_state.v1",
            CANIC_CONTROL_PLANE_MAX_ID,
        )
        .expect("last control-plane slot");
    }

    #[test]
    fn rejects_canic_framework_namespaces_outside_owned_ranges() {
        let err = validate("canic.core.app_state.v1", CANIC_CONTROL_PLANE_MIN_ID)
            .expect_err("core key cannot claim control-plane range");
        assert!(matches!(
            err,
            MemoryRegistryError::RangeAuthorityViolation { .. }
        ));

        let err = validate(
            "canic.control_plane.template_manifest.v1",
            CANIC_CORE_MIN_ID,
        )
        .expect_err("control-plane key cannot claim core range");
        assert!(matches!(
            err,
            MemoryRegistryError::RangeAuthorityViolation { .. }
        ));

        let err = validate("canic.unknown.state.v1", CANIC_CONTROL_PLANE_MAX_ID + 1)
            .expect_err("unknown canic namespace is reserved");
        assert!(matches!(
            err,
            MemoryRegistryError::RangeAuthorityViolation { .. }
        ));
    }

    #[test]
    fn accepts_application_keys_only_outside_reserved_ranges() {
        validate("app.users.v1", CANIC_CONTROL_PLANE_MAX_ID + 1).expect("application slot");
        validate("app.archive.v1", ic_memory::MEMORY_MANAGER_MAX_ID).expect("last app slot");

        let err = validate("app.users.v1", CANIC_CORE_MIN_ID)
            .expect_err("application key cannot claim Canic core range");
        assert!(matches!(
            err,
            MemoryRegistryError::RangeAuthorityViolation { .. }
        ));

        let err = validate("app.users.v1", ic_memory::MEMORY_MANAGER_LEDGER_ID)
            .expect_err("application key cannot claim ic-memory governance range");
        assert!(matches!(
            err,
            MemoryRegistryError::RangeAuthorityViolation { .. }
        ));
    }

    #[test]
    fn rejects_application_reservations() {
        let err = validate_reserved("app.users.v1", CANIC_CONTROL_PLANE_MAX_ID + 1)
            .expect_err("Canic does not pre-reserve application keys");
        assert!(matches!(
            err,
            MemoryRegistryError::RangeAuthorityViolation { .. }
        ));
    }
}