Skip to main content

ic_storage/runtime/
admission.rs

1//! Canonical declarations, namespace grants and composable host admission.
2use crate::{AllocationId, StableKey};
3use std::collections::BTreeSet;
4
5/// Invalid host declarations or grants; no allocation authority has been granted.
6#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
7pub enum AdmissionError {
8    /// Invalid owner label or namespace.
9    #[error("invalid authority or namespace")]
10    Invalid,
11    /// Stable-key grammar failure.
12    #[error(transparent)]
13    Key(#[from] crate::StableKeyError),
14    /// Duplicate or overlapping declaration/grant.
15    #[error("duplicate key or overlapping namespace")]
16    Duplicate,
17    /// The host has not granted this owner the key's namespace.
18    #[error("namespace is not granted to this authority")]
19    Unauthorized,
20    /// Historical selection is unknown or exceeds the configured count.
21    #[error("unknown historical allocation or declaration budget exceeded")]
22    Limit,
23}
24fn owner_valid(owner: &str) -> bool {
25    !owner.is_empty()
26        && owner.len() <= 128
27        && owner
28            .bytes()
29            .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
30}
31
32/// Host grant for one owner and a dot-terminated namespace.
33#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct MemoryAuthority {
35    authority: String,
36    prefix: String,
37}
38impl MemoryAuthority {
39    /// Check owner and namespace syntax. Names do not select numeric IDs.
40    ///
41    /// # Errors
42    /// Rejects invalid authority labels or namespace prefixes.
43    pub fn new(
44        authority: impl Into<String>,
45        prefix: impl Into<String>,
46    ) -> Result<Self, AdmissionError> {
47        let authority = authority.into();
48        let prefix = prefix.into();
49        if !owner_valid(&authority) || !crate::key::is_key_prefix(&prefix) {
50            return Err(AdmissionError::Invalid);
51        }
52        Ok(Self { authority, prefix })
53    }
54}
55
56/// Canonical, non-overlapping namespace grants for a host composition.
57#[derive(Clone, Debug, Eq, PartialEq)]
58pub struct AllocationGrants(Vec<MemoryAuthority>);
59impl AllocationGrants {
60    /// Validate namespace overlap before runtime bootstrap.
61    ///
62    /// # Errors
63    /// Rejects duplicate or overlapping namespace grants.
64    pub fn new(mut grants: Vec<MemoryAuthority>) -> Result<Self, AdmissionError> {
65        grants.sort_unstable_by(|a, b| a.prefix.cmp(&b.prefix));
66        if grants
67            .windows(2)
68            .any(|pair| pair[1].prefix.starts_with(&pair[0].prefix))
69        {
70            return Err(AdmissionError::Duplicate);
71        }
72        Ok(Self(grants))
73    }
74    pub(super) fn check(&self, request: &MemoryRequest) -> Result<(), AdmissionError> {
75        if self.0.iter().any(|grant| {
76            grant.authority == request.authority && request.key.as_str().starts_with(&grant.prefix)
77        }) {
78            Ok(())
79        } else {
80            Err(AdmissionError::Unauthorized)
81        }
82    }
83}
84
85/// Checked request naming a store under one host-granted authority.
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct MemoryRequest {
88    authority: String,
89    key: StableKey,
90}
91impl MemoryRequest {
92    /// Construct a key-only allocation request.
93    ///
94    /// # Errors
95    /// Rejects invalid authority labels or stable-key syntax.
96    pub fn new(authority: impl Into<String>, key: &str) -> Result<Self, AdmissionError> {
97        let authority = authority.into();
98        if !owner_valid(&authority) {
99            return Err(AdmissionError::Invalid);
100        }
101        Ok(Self {
102            authority,
103            key: StableKey::parse(key)?,
104        })
105    }
106    /// Declaring authority label.
107    #[must_use]
108    pub fn authority(&self) -> &str {
109        &self.authority
110    }
111    /// Requested durable name.
112    #[must_use]
113    pub const fn stable_key(&self) -> &StableKey {
114        &self.key
115    }
116}
117
118/// Immutable canonical request set, with duplicate names rejected.
119#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct SealedDeclarationSnapshot(Vec<MemoryRequest>);
121impl SealedDeclarationSnapshot {
122    /// Seal the complete host-composed requests.
123    ///
124    /// # Errors
125    /// Rejects duplicate stable keys in the declaration set.
126    pub fn new(mut requests: Vec<MemoryRequest>) -> Result<Self, AdmissionError> {
127        requests.sort_unstable_by(|a, b| a.key.cmp(&b.key));
128        if requests.windows(2).any(|pair| pair[0].key == pair[1].key) {
129            return Err(AdmissionError::Duplicate);
130        }
131        Ok(Self(requests))
132    }
133    /// Borrow checked requests.
134    #[must_use]
135    pub fn requests(&self) -> &[MemoryRequest] {
136        &self.0
137    }
138}
139
140/// Host-selected token binding the exact configured policy to a runtime.
141#[derive(Clone, Copy, Debug, Eq, PartialEq)]
142pub struct PolicyIdentity([u8; 32]);
143impl PolicyIdentity {
144    /// Construct a complete policy identity.
145    #[must_use]
146    pub const fn new(bytes: [u8; 32]) -> Self {
147        Self(bytes)
148    }
149}
150
151/// Read-only recovered entries and explicit current historical admission.
152pub struct BootstrapAdmission<'a> {
153    grants: &'a AllocationGrants,
154    recovered: &'a [(StableKey, AllocationId, u64)],
155    selected: Vec<MemoryRequest>,
156    keys: BTreeSet<StableKey>,
157    maximum: usize,
158    failure: Option<AdmissionError>,
159}
160impl<'a> BootstrapAdmission<'a> {
161    pub(super) fn new(
162        source: &SealedDeclarationSnapshot,
163        grants: &'a AllocationGrants,
164        recovered: &'a [(StableKey, AllocationId, u64)],
165        maximum: usize,
166    ) -> Result<Self, AdmissionError> {
167        if source.0.len() > maximum {
168            return Err(AdmissionError::Limit);
169        }
170        for request in &source.0 {
171            grants.check(request)?;
172        }
173        Ok(Self {
174            grants,
175            recovered,
176            selected: source.0.clone(),
177            keys: source.0.iter().map(|request| request.key.clone()).collect(),
178            maximum,
179            failure: None,
180        })
181    }
182    /// Retained names, identities and extents. This grants no memory access.
183    #[must_use]
184    pub const fn recovered_allocations(&self) -> &[(StableKey, AllocationId, u64)] {
185        self.recovered
186    }
187    /// Whether this bootstrap has selected a key.
188    #[must_use]
189    pub fn is_declared(&self, key: &StableKey) -> bool {
190        self.keys.contains(key)
191    }
192    /// Select a retained name under current grants. An ignored failure still rejects bootstrap.
193    ///
194    /// # Errors
195    /// Rejects invalid or unauthorized names, missing retained entries, duplicates, capacity limits, or a previous admission failure.
196    pub fn include_historical(&mut self, authority: &str, key: &str) -> Result<(), AdmissionError> {
197        if let Some(error) = &self.failure {
198            return Err(error.clone());
199        }
200        let result = self.include(authority, key);
201        if let Err(error) = &result {
202            self.failure = Some(error.clone());
203        }
204        result
205    }
206    fn include(&mut self, authority: &str, key: &str) -> Result<(), AdmissionError> {
207        let request = MemoryRequest::new(authority, key)?;
208        self.grants.check(&request)?;
209        if self.selected.len() >= self.maximum
210            || !self.recovered.iter().any(|(key, _, _)| key == &request.key)
211        {
212            return Err(AdmissionError::Limit);
213        }
214        if !self.keys.insert(request.key.clone()) {
215            return Err(AdmissionError::Duplicate);
216        }
217        self.selected.push(request);
218        Ok(())
219    }
220    pub(super) fn complete(self) -> Result<Vec<MemoryRequest>, AdmissionError> {
221        if let Some(error) = self.failure {
222            Err(error)
223        } else {
224            Ok(self.selected)
225        }
226    }
227}
228
229/// Host-specific recovered-state and key admission, before durable commitment.
230pub trait RuntimeBootstrapPolicy {
231    /// Policy-specific rejection.
232    type Error;
233    /// Validate one admitted key; numeric placement is owned by Storage.
234    ///
235    /// # Errors
236    /// Returns the host policy error when this key is rejected.
237    fn validate_allocation(&self, key: &StableKey) -> Result<(), Self::Error>;
238    /// Inspect retained names and optionally select historical keys.
239    ///
240    /// # Errors
241    /// Returns the host policy error when recovered-state inspection rejects admission.
242    fn prepare_bootstrap(
243        &self,
244        _admission: &mut BootstrapAdmission<'_>,
245    ) -> Result<(), Self::Error> {
246        Ok(())
247    }
248    /// Bind all effective host policy configuration.
249    fn runtime_bootstrap_identity(&self) -> PolicyIdentity;
250}
251/// Standard policy relying on Storage's namespace admission and resource limits.
252pub struct GenericAllocationPolicy;
253impl RuntimeBootstrapPolicy for GenericAllocationPolicy {
254    type Error = std::convert::Infallible;
255    fn validate_allocation(&self, _key: &StableKey) -> Result<(), Self::Error> {
256        Ok(())
257    }
258    fn runtime_bootstrap_identity(&self) -> PolicyIdentity {
259        PolicyIdentity::new(*b"ic-storage.noop-policy..........")
260    }
261}