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