Skip to main content

ic_memory/
policy.rs

1use crate::{
2    constants::DIAGNOSTIC_STRING_MAX_BYTES,
3    key::StableKey,
4    slot::MemoryManagerSlot,
5    text::{DiagnosticTextError, validate_diagnostic_text},
6};
7use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
8
9///
10/// PolicyIdentity
11///
12/// Bounded semantic identity for one runtime bootstrap policy configuration.
13///
14/// The name identifies the policy family, `version` changes when its semantics
15/// change, and the optional digest distinguishes runtime configuration. The
16/// digest is supplied by the policy implementation; ic-memory does not choose
17/// or compute a hashing algorithm.
18///
19/// This identity is an in-memory repeat-call and diagnostic binding. It is not
20/// persisted to the allocation ledger.
21///
22
23#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
24pub struct PolicyIdentity {
25    name: Box<str>,
26    version: u32,
27    configuration_digest: Option<[u8; 32]>,
28}
29
30#[derive(Deserialize)]
31#[serde(deny_unknown_fields)]
32struct PolicyIdentityRepresentation {
33    name: String,
34    version: u32,
35    #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
36    configuration_digest: Option<[u8; 32]>,
37}
38
39impl<'de> Deserialize<'de> for PolicyIdentity {
40    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41    where
42        D: Deserializer<'de>,
43    {
44        let representation = PolicyIdentityRepresentation::deserialize(deserializer)?;
45        let mut identity =
46            Self::new(representation.name, representation.version).map_err(D::Error::custom)?;
47        identity.configuration_digest = representation.configuration_digest;
48        Ok(identity)
49    }
50}
51
52impl PolicyIdentity {
53    /// Construct a validated policy identity without a configuration digest.
54    pub fn new(name: impl Into<String>, version: u32) -> Result<Self, PolicyIdentityError> {
55        let name = name.into();
56        validate_policy_identity_name(&name)?;
57        if version == 0 {
58            return Err(PolicyIdentityError::ZeroVersion);
59        }
60        Ok(Self {
61            name: name.into_boxed_str(),
62            version,
63            configuration_digest: None,
64        })
65    }
66
67    /// Attach a caller-computed configuration digest.
68    #[must_use]
69    pub const fn with_configuration_digest(mut self, digest: [u8; 32]) -> Self {
70        self.configuration_digest = Some(digest);
71        self
72    }
73
74    /// Borrow the bounded policy-family name.
75    #[must_use]
76    pub fn name(&self) -> &str {
77        &self.name
78    }
79
80    /// Return the nonzero semantic policy version.
81    #[must_use]
82    pub const fn version(&self) -> u32 {
83        self.version
84    }
85
86    /// Borrow the optional caller-computed configuration digest.
87    #[must_use]
88    pub const fn configuration_digest(&self) -> Option<&[u8; 32]> {
89        self.configuration_digest.as_ref()
90    }
91}
92
93///
94/// PolicyIdentityError
95///
96/// Failure to construct a bounded runtime bootstrap policy identity.
97///
98
99#[non_exhaustive]
100#[derive(Clone, Copy, Debug, Eq, thiserror::Error, PartialEq)]
101pub enum PolicyIdentityError {
102    /// Policy-family names must not be empty.
103    #[error("runtime bootstrap policy identity name must not be empty")]
104    EmptyName,
105    /// Policy-family names must remain bounded diagnostic metadata.
106    #[error("runtime bootstrap policy identity name is {length} bytes; maximum is {maximum} bytes")]
107    NameTooLong {
108        /// Actual UTF-8 byte length.
109        length: usize,
110        /// Maximum accepted byte length.
111        maximum: usize,
112    },
113    /// Policy-family names must not require Unicode normalization.
114    #[error("runtime bootstrap policy identity name must be ASCII")]
115    NonAsciiName,
116    /// Policy-family names must be printable diagnostic metadata.
117    #[error("runtime bootstrap policy identity name must not contain ASCII control characters")]
118    ControlCharacterName,
119    /// Semantic policy version zero is reserved as invalid.
120    #[error("runtime bootstrap policy identity version must be greater than zero")]
121    ZeroVersion,
122}
123
124fn validate_policy_identity_name(name: &str) -> Result<(), PolicyIdentityError> {
125    validate_diagnostic_text(name).map_err(|error| match error {
126        DiagnosticTextError::Empty => PolicyIdentityError::EmptyName,
127        DiagnosticTextError::TooLong => PolicyIdentityError::NameTooLong {
128            length: name.len(),
129            maximum: DIAGNOSTIC_STRING_MAX_BYTES,
130        },
131        DiagnosticTextError::NonAscii => PolicyIdentityError::NonAsciiName,
132        DiagnosticTextError::ControlCharacter => PolicyIdentityError::ControlCharacterName,
133    })
134}
135
136///
137/// AllocationPolicy
138///
139/// Framework-supplied rules for whether a key may claim a slot.
140///
141/// Policy is intentionally separate from the durable ledger invariant. The
142/// ledger remembers `stable_key -> allocation_slot`; this trait lets an
143/// integration reject declarations that do not belong to its namespace or
144/// substrate-specific range before staging a generation.
145///
146/// In the default `MemoryManager` runtime, registered range claims are checked
147/// before this policy, and this policy receives external declarations only.
148/// The internal allocation-ledger declaration remains exclusively governed by
149/// ic-memory. Framework adapters should decide whether registered range claims
150/// or their own policy is authoritative for application ID space, then register
151/// ranges accordingly.
152///
153
154pub trait AllocationPolicy {
155    /// Policy error type.
156    type Error;
157
158    /// Validate a stable key against framework naming rules.
159    fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error>;
160
161    /// Validate a stable-key to allocation-slot claim.
162    /// The slot already contains a usable ID; policy checks ownership and scope.
163    fn validate_slot(&self, key: &StableKey, slot: &MemoryManagerSlot) -> Result<(), Self::Error>;
164
165    /// Validate a reserved stable-key to allocation-slot claim.
166    /// The checked slot does not establish permission to reserve that ID.
167    fn validate_reserved_slot(
168        &self,
169        key: &StableKey,
170        slot: &MemoryManagerSlot,
171    ) -> Result<(), Self::Error>;
172}
173
174///
175/// RuntimeBootstrapPolicy
176///
177/// Allocation policy with an explicit semantic identity for runtime bootstrap.
178///
179/// [`crate::MemoryRuntime`] binds its successful bootstrap to this identity.
180/// Repeated bootstrap is idempotent only when the caller supplies the same
181/// sealed declaration snapshot and the same policy identity. Implementations
182/// should change the identity whenever policy configuration or semantics
183/// change. Configuration-dependent policies should include a digest derived
184/// from their effective configuration.
185///
186
187pub trait RuntimeBootstrapPolicy: AllocationPolicy {
188    /// Admit recovered identity and complete declarations before resolution.
189    ///
190    /// Runs once per cold bootstrap attempt after validated recovery. Warm
191    /// bootstrap/adoption does not replay it. The default selects no historical
192    /// keys. Hosts compose generated consumers here under their existing policy
193    /// and bucket profile. Include these semantics in the policy identity.
194    fn prepare_bootstrap(
195        &self,
196        _admission: &mut crate::BootstrapAdmission<'_>,
197    ) -> Result<(), Self::Error> {
198        Ok(())
199    }
200
201    /// Construct the bounded semantic identity of this policy configuration.
202    fn runtime_bootstrap_identity(&self) -> Result<PolicyIdentity, PolicyIdentityError>;
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn policy_identity_validates_name_version_and_digest() {
211        let digest = [0xA5; 32];
212        let identity = PolicyIdentity::new("canic.memory-bootstrap-policy", 1)
213            .expect("valid identity")
214            .with_configuration_digest(digest);
215
216        assert_eq!(identity.name(), "canic.memory-bootstrap-policy");
217        assert_eq!(identity.version(), 1);
218        assert_eq!(identity.configuration_digest(), Some(&digest));
219    }
220
221    #[test]
222    fn policy_identity_rejects_unbounded_or_noncanonical_metadata() {
223        assert_eq!(
224            PolicyIdentity::new("", 1).expect_err("empty name"),
225            PolicyIdentityError::EmptyName
226        );
227        assert!(matches!(
228            PolicyIdentity::new("x".repeat(DIAGNOSTIC_STRING_MAX_BYTES + 1), 1),
229            Err(PolicyIdentityError::NameTooLong { .. })
230        ));
231        assert_eq!(
232            PolicyIdentity::new("policy\nname", 1).expect_err("control character"),
233            PolicyIdentityError::ControlCharacterName
234        );
235        assert_eq!(
236            PolicyIdentity::new("policé", 1).expect_err("non-ASCII"),
237            PolicyIdentityError::NonAsciiName
238        );
239        assert_eq!(
240            PolicyIdentity::new("policy", 0).expect_err("zero version"),
241            PolicyIdentityError::ZeroVersion
242        );
243    }
244
245    #[test]
246    fn policy_identity_deserialization_revalidates_invariants() {
247        #[derive(Serialize)]
248        struct UncheckedPolicyIdentity<'a> {
249            name: &'a str,
250            version: u32,
251            configuration_digest: Option<[u8; 32]>,
252        }
253
254        let bytes = crate::test_cbor::to_vec(&UncheckedPolicyIdentity {
255            name: "",
256            version: 1,
257            configuration_digest: None,
258        })
259        .expect("invalid diagnostic bytes");
260        let error = crate::test_cbor::from_slice::<PolicyIdentity>(&bytes)
261            .expect_err("deserialization must revalidate identity");
262        assert!(error.to_string().contains("must not be empty"));
263    }
264}