Skip to main content

ic_storage/runtime/
mod.rs

1//! Named allocation admission and exclusive leases over the owned directory.
2mod admission;
3mod custody;
4mod directory_model;
5
6use crate::{Memory, StableKey};
7pub use admission::{
8    AdmissionError, AllocationGrants, BootstrapAdmission, GenericAllocationPolicy, MemoryAuthority,
9    MemoryRequest, PolicyIdentity, RuntimeBootstrapPolicy, SealedDeclarationSnapshot,
10};
11pub use custody::{BackingIdentity, MemoryBacking};
12pub use directory_model::Error as DirectoryError;
13use directory_model::leases::{Lease, Owner};
14pub use directory_model::{AllocationId, StorageConfig};
15use std::collections::BTreeSet;
16
17/// Construction refuses aliased or malformed backing without resetting it.
18#[derive(Debug, thiserror::Error)]
19pub enum RuntimeConstructionError {
20    /// Another runtime or detached lease retains custody.
21    #[error("backing already claimed")]
22    BackingAlreadyClaimed,
23    /// The current owned directory could not be loaded or initialized.
24    #[error(transparent)]
25    Directory(#[from] DirectoryError),
26}
27
28/// Allocation growth failures before authority publication.
29#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
30pub enum RuntimeGrowError {
31    /// Requested page arithmetic overflowed.
32    #[error("allocation page count overflow")]
33    ArithmeticOverflow,
34    /// The configured allocation or directory resource budget was exceeded.
35    #[error("allocation capacity exceeded")]
36    CapacityExceeded,
37    /// Physical backing capacity was refused.
38    #[error("backing refused growth by {additional_pages} pages")]
39    BackingRefused { additional_pages: u64 },
40    /// A previous backing failure poisoned this owner.
41    #[error("allocation owner is poisoned")]
42    Poisoned,
43    /// Directory validation failed at a checked boundary.
44    #[error("allocation directory validation failed")]
45    InvalidDirectory,
46}
47
48/// A key cannot be opened until current host admission succeeds.
49#[derive(Debug, thiserror::Error)]
50pub enum RuntimeOpenError {
51    /// Bootstrap has not committed current authority.
52    #[error("runtime is not bootstrapped")]
53    NotBootstrapped,
54    /// The key is absent from the current admitted declarations.
55    #[error("key is not admitted: {0}")]
56    NotAdmitted(StableKey),
57    /// An exclusive lease for this allocation is already alive.
58    #[error("allocation {id} already open")]
59    AlreadyOpen { id: u64 },
60    /// The directory refused opening.
61    #[error(transparent)]
62    Directory(#[from] DirectoryError),
63}
64
65/// Bootstrap failed before current authority could be published.
66#[derive(Debug, thiserror::Error)]
67pub enum RuntimeBootstrapError<E> {
68    /// Host declarations or namespace grants were invalid.
69    #[error(transparent)]
70    Admission(#[from] AdmissionError),
71    /// The host's policy rejected the preparation or an allocation.
72    #[error("host policy rejected allocation")]
73    Policy(E),
74    /// Current bootstrap inputs differ from the established binding.
75    #[error("bootstrap binding mismatch")]
76    BindingMismatch,
77    /// A shared verification cannot bootstrap an uninitialized owner.
78    #[error("runtime is not bootstrapped")]
79    NotBootstrapped,
80    /// Durable directory commitment failed.
81    #[error(transparent)]
82    Directory(#[from] DirectoryError),
83}
84
85struct Binding {
86    source: SealedDeclarationSnapshot,
87    grants: AllocationGrants,
88    policy: PolicyIdentity,
89    admitted: BTreeSet<StableKey>,
90}
91
92///
93/// MemoryRuntime
94///
95/// One custodian of named allocations on a backing. Bootstrap validates namespace
96/// grants and host policy, commits bindings, then permits exclusive leases.
97/// Omitted retained names keep their allocation, but require current admission
98/// before opening. IDs are opaque u64 values, never selected by applications.
99///
100pub struct MemoryRuntime<M: Memory> {
101    owner: Owner<M>,
102    config: StorageConfig,
103    binding: Option<Binding>,
104}
105
106impl<M: Memory> MemoryRuntime<M> {
107    /// Claim backing and create or read its owned directory. Never reset nonempty backing.
108    pub fn new(memory: M) -> Result<Self, RuntimeConstructionError>
109    where
110        M: MemoryBacking,
111    {
112        Self::new_with_config(memory, StorageConfig::default())
113    }
114    /// Use explicit directory resource limits; reopening revalidates under those limits.
115    pub fn new_with_config(
116        memory: M,
117        config: StorageConfig,
118    ) -> Result<Self, RuntimeConstructionError>
119    where
120        M: MemoryBacking,
121    {
122        let owner = Owner::init(memory, config).map_err(|error| match error {
123            DirectoryError::AlreadyClaimed => RuntimeConstructionError::BackingAlreadyClaimed,
124            error => RuntimeConstructionError::Directory(error),
125        })?;
126        Ok(Self {
127            owner,
128            config,
129            binding: None,
130        })
131    }
132    /// Whether current host authority has been committed.
133    #[must_use]
134    pub const fn is_bootstrapped(&self) -> bool {
135        self.binding.is_some()
136    }
137    /// Current durable directory generation, after bootstrap admission.
138    pub fn committed_generation(&self) -> Result<u64, RuntimeOpenError> {
139        if self.binding.is_none() {
140            return Err(RuntimeOpenError::NotBootstrapped);
141        }
142        Ok(self.owner.generation())
143    }
144    /// Effective resource admission limits.
145    #[must_use]
146    pub const fn config(&self) -> StorageConfig {
147        self.config
148    }
149    /// Admit a complete declaration set, retaining omitted historical bindings.
150    pub fn bootstrap<P: RuntimeBootstrapPolicy>(
151        &mut self,
152        source: &SealedDeclarationSnapshot,
153        grants: &AllocationGrants,
154        policy: &P,
155    ) -> Result<u64, RuntimeBootstrapError<P::Error>> {
156        if self.binding.is_some() {
157            return self.verify_bootstrap(source, grants, policy);
158        }
159        self.owner.verify()?;
160        let policy_identity = policy.runtime_bootstrap_identity();
161        let bindings = self.owner.bindings();
162        let mut admission =
163            BootstrapAdmission::new(source, grants, &bindings, self.config.allocations)?;
164        let result = policy.prepare_bootstrap(&mut admission);
165        let requests = admission.complete()?;
166        result.map_err(RuntimeBootstrapError::Policy)?;
167        for request in &requests {
168            policy
169                .validate_allocation(request.stable_key())
170                .map_err(RuntimeBootstrapError::Policy)?;
171        }
172        let admitted: BTreeSet<_> = requests
173            .into_iter()
174            .map(|request| request.stable_key().clone())
175            .collect();
176        self.owner.bind(admitted.iter().cloned().collect())?;
177        self.binding = Some(Binding {
178            source: source.clone(),
179            grants: grants.clone(),
180            policy: policy_identity,
181            admitted,
182        });
183        Ok(self.owner.generation())
184    }
185    /// Compare the complete established binding without IO or policy callbacks.
186    pub fn verify_bootstrap<P: RuntimeBootstrapPolicy>(
187        &self,
188        source: &SealedDeclarationSnapshot,
189        grants: &AllocationGrants,
190        policy: &P,
191    ) -> Result<u64, RuntimeBootstrapError<P::Error>> {
192        let binding = self
193            .binding
194            .as_ref()
195            .ok_or(RuntimeBootstrapError::NotBootstrapped)?;
196        if binding.source != *source
197            || binding.grants != *grants
198            || binding.policy != policy.runtime_bootstrap_identity()
199        {
200            return Err(RuntimeBootstrapError::BindingMismatch);
201        }
202        Ok(self.owner.generation())
203    }
204    /// Open one currently admitted name, preserving custody through detached handles.
205    ///
206    /// # Panics
207    /// Propagates backing failures or a broken internal binding invariant.
208    pub fn open_memory(&self, key: &StableKey) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
209        let binding = self
210            .binding
211            .as_ref()
212            .ok_or(RuntimeOpenError::NotBootstrapped)?;
213        if !binding.admitted.contains(key) {
214            return Err(RuntimeOpenError::NotAdmitted(key.clone()));
215        }
216        let lease = self.owner.lease(key).map_err(|error| match error {
217            DirectoryError::AlreadyOpen => RuntimeOpenError::AlreadyOpen {
218                id: self.allocation_id(key).unwrap().get(),
219            },
220            error => RuntimeOpenError::Directory(error),
221        })?;
222        Ok(RuntimeMemory { lease })
223    }
224    /// Verify component requirements against current host admission without IO.
225    pub fn verify_authority(
226        &self,
227        requirements: &SealedDeclarationSnapshot,
228        authority: &str,
229    ) -> Result<(), RuntimeOpenError> {
230        let binding = self
231            .binding
232            .as_ref()
233            .ok_or(RuntimeOpenError::NotBootstrapped)?;
234        for request in requirements.requests() {
235            if request.authority() != authority
236                || !binding.admitted.contains(request.stable_key())
237                || binding.grants.check(request).is_err()
238            {
239                return Err(RuntimeOpenError::NotAdmitted(request.stable_key().clone()));
240            }
241        }
242        Ok(())
243    }
244    /// Resolve a retained identity for diagnostics; this grants no access.
245    #[must_use]
246    pub fn allocation_id(&self, key: &StableKey) -> Option<AllocationId> {
247        self.owner.allocation_id(key)
248    }
249    /// Observe checked directory entries and actual logical extents.
250    pub fn memory_allocations(&self) -> Result<Vec<MemoryAllocation>, DirectoryError> {
251        self.owner.verify()?;
252        Ok(self
253            .owner
254            .bindings()
255            .into_iter()
256            .map(|(stable_key, id, pages)| MemoryAllocation {
257                stable_key,
258                id,
259                pages,
260            })
261            .collect())
262    }
263}
264
265///
266/// MemoryAllocation
267///
268/// Passive diagnostic entry; no lease or allocation authority is carried.
269///
270#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
271pub struct MemoryAllocation {
272    /// Durable name.
273    pub stable_key: StableKey,
274    /// Opaque identity.
275    pub id: AllocationId,
276    /// Logical extent in Wasm pages.
277    pub pages: u64,
278}
279
280///
281/// RuntimeMemory
282///
283/// Exclusive non-cloneable allocation lease. IO trusts custody between checked
284/// boundaries; growth commits the directory before publishing its new extent.
285/// A detached lease retains the backing claim until it is dropped.
286///
287pub struct RuntimeMemory<M: Memory> {
288    lease: Lease<M>,
289}
290impl<M: Memory> RuntimeMemory<M> {
291    /// Logical page extent.
292    #[must_use]
293    pub fn size(&self) -> u64 {
294        self.lease.size()
295    }
296    /// Reserve additional zero-filled logical pages without exposing backing.
297    pub fn grow(&self, pages: u64) -> Result<u64, RuntimeGrowError> {
298        self.lease.try_grow(pages).map_err(|error| match error {
299            DirectoryError::ArithmeticOverflow => RuntimeGrowError::ArithmeticOverflow,
300            DirectoryError::Growth => RuntimeGrowError::BackingRefused {
301                additional_pages: pages,
302            },
303            DirectoryError::Budget | DirectoryError::Wire(_) => RuntimeGrowError::CapacityExceeded,
304            DirectoryError::Poisoned => RuntimeGrowError::Poisoned,
305            _ => RuntimeGrowError::InvalidDirectory,
306        })
307    }
308    /// Read a completely bounds-checked logical range.
309    pub fn read(&self, offset: u64, bytes: &mut [u8]) {
310        self.lease.read(offset, bytes);
311    }
312    /// Write a completely bounds-checked logical range.
313    pub fn write(&self, offset: u64, bytes: &[u8]) {
314        self.lease.write(offset, bytes);
315    }
316}
317impl<M: Memory> Memory for RuntimeMemory<M> {
318    fn size(&self) -> u64 {
319        self.size()
320    }
321    fn grow(&self, pages: u64) -> i64 {
322        self.grow(pages)
323            .map_or(-1, |previous| i64::try_from(previous).expect("page count"))
324    }
325    fn read(&self, offset: u64, bytes: &mut [u8]) {
326        self.read(offset, bytes);
327    }
328    fn write(&self, offset: u64, bytes: &[u8]) {
329        self.write(offset, bytes);
330    }
331}