Skip to main content

ic_memory/
policy.rs

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