Skip to main content

ic_memory/
bootstrap.rs

1use crate::{
2    declaration::AllocationDeclaration,
3    declaration::DeclarationSnapshot,
4    ledger::{
5        AllocationLedger, AllocationReservationError, AllocationRetirement,
6        AllocationRetirementError, AllocationStageError, LedgerCommitError, LedgerCommitStore,
7    },
8    policy::AllocationPolicy,
9    session::ValidatedAllocations,
10    validation::{AllocationValidationError, validate_allocations},
11};
12
13///
14/// AllocationBootstrap
15///
16/// Golden-path allocation ledger bootstrap pipeline.
17///
18/// This type owns allocation-governance sequencing only: recover the persisted
19/// ledger, apply the owner layer's policy, validate current declarations
20/// against ledger history, stage and commit the next generation, and return
21/// [`ValidatedAllocations`] only after the commit succeeds.
22///
23/// `AllocationBootstrap` is for whichever layer owns a given `ic-memory`
24/// ledger store. That owner may be a framework such as Canic, a library such as
25/// IcyDB using `ic-memory` directly, or a standalone application canister. The
26/// ownership model is not a fixed `ic-memory -> Canic -> IcyDB -> application`
27/// chain.
28///
29/// Exactly one owner should bootstrap a given ledger store. If multiple layers
30/// use `ic-memory` in the same canister, they must either compose their
31/// declarations into one bootstrap owner or use distinct ledger stores and
32/// allocation domains.
33///
34/// The owner still decides when bootstrap runs, how the ledger store is backed
35/// by stable memory, and when endpoint dispatch or stable-memory handle opening
36/// is allowed.
37#[derive(Debug)]
38pub struct AllocationBootstrap<'store> {
39    store: &'store mut LedgerCommitStore,
40}
41
42impl<'store> AllocationBootstrap<'store> {
43    /// Build a bootstrap pipeline over a protected ledger commit store.
44    pub const fn new(store: &'store mut LedgerCommitStore) -> Self {
45        Self { store }
46    }
47
48    /// Recover, validate, stage, commit, and publish one allocation generation.
49    pub fn validate_and_commit<P>(
50        &mut self,
51        snapshot: DeclarationSnapshot,
52        policy: &P,
53        committed_at: Option<u64>,
54    ) -> Result<BootstrapCommit, BootstrapError<P::Error>>
55    where
56        P: AllocationPolicy,
57    {
58        let prior = self.store.recover().map_err(BootstrapError::Ledger)?;
59        self.validate_against(prior, snapshot, policy, committed_at)
60    }
61
62    /// Initialize an empty ledger store explicitly, then validate and commit.
63    ///
64    /// This is the privileged genesis/import path. Normal default-runtime users
65    /// should use [`crate::bootstrap_default_memory_manager`], which supplies an
66    /// empty current-format genesis ledger. A non-empty `genesis` should only be
67    /// supplied by the layer that owns migration or import for this ledger store.
68    ///
69    /// The generic crate guarantees only that `genesis` is used when the
70    /// protected physical store is empty, never when recovery sees corrupt or
71    /// partially written state.
72    pub fn initialize_validate_and_commit<P>(
73        &mut self,
74        genesis: &AllocationLedger,
75        snapshot: DeclarationSnapshot,
76        policy: &P,
77        committed_at: Option<u64>,
78    ) -> Result<BootstrapCommit, BootstrapError<P::Error>>
79    where
80        P: AllocationPolicy,
81    {
82        let prior = self
83            .store
84            .recover_or_initialize(genesis)
85            .map_err(BootstrapError::Ledger)?;
86        self.validate_against(prior, snapshot, policy, committed_at)
87    }
88
89    /// Recover, policy-check, reserve, and commit one reservation generation.
90    pub fn reserve_and_commit<P>(
91        &mut self,
92        reservations: &[AllocationDeclaration],
93        policy: &P,
94        committed_at: Option<u64>,
95    ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
96    where
97        P: AllocationPolicy,
98    {
99        let prior = self
100            .store
101            .recover()
102            .map_err(BootstrapReservationError::Ledger)?;
103        self.reserve_against(prior.into_ledger(), reservations, policy, committed_at)
104    }
105
106    /// Initialize an empty ledger store, then reserve and commit.
107    ///
108    /// This is the privileged genesis/import path for reservation staging. A
109    /// non-empty `genesis` should only be supplied by the owner of migration or
110    /// import for this ledger store.
111    pub fn initialize_reserve_and_commit<P>(
112        &mut self,
113        genesis: &AllocationLedger,
114        reservations: &[AllocationDeclaration],
115        policy: &P,
116        committed_at: Option<u64>,
117    ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
118    where
119        P: AllocationPolicy,
120    {
121        let prior = self
122            .store
123            .recover_or_initialize(genesis)
124            .map_err(BootstrapReservationError::Ledger)?;
125        self.reserve_against(prior.into_ledger(), reservations, policy, committed_at)
126    }
127
128    /// Recover, retire, and commit one explicit retirement generation.
129    pub fn retire_and_commit(
130        &mut self,
131        retirement: &AllocationRetirement,
132        committed_at: Option<u64>,
133    ) -> Result<AllocationLedger, BootstrapRetirementError> {
134        let prior = self
135            .store
136            .recover()
137            .map_err(BootstrapRetirementError::Ledger)?;
138        self.retire_against(prior.into_ledger(), retirement, committed_at)
139    }
140
141    fn reserve_against<P>(
142        &mut self,
143        prior: AllocationLedger,
144        reservations: &[AllocationDeclaration],
145        policy: &P,
146        committed_at: Option<u64>,
147    ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
148    where
149        P: AllocationPolicy,
150    {
151        for reservation in reservations {
152            policy
153                .validate_key(&reservation.stable_key)
154                .map_err(BootstrapReservationError::Policy)?;
155            policy
156                .validate_reserved_slot(&reservation.stable_key, &reservation.slot)
157                .map_err(BootstrapReservationError::Policy)?;
158        }
159
160        let staged = prior
161            .stage_reservation_generation(reservations, committed_at)
162            .map_err(BootstrapReservationError::Reservation)?;
163        self.store
164            .commit(&staged)
165            .map(crate::RecoveredLedger::into_ledger)
166            .map_err(BootstrapReservationError::Ledger)
167    }
168
169    fn retire_against(
170        &mut self,
171        prior: AllocationLedger,
172        retirement: &AllocationRetirement,
173        committed_at: Option<u64>,
174    ) -> Result<AllocationLedger, BootstrapRetirementError> {
175        let staged = prior
176            .stage_retirement_generation(retirement, committed_at)
177            .map_err(BootstrapRetirementError::Retirement)?;
178        self.store
179            .commit(&staged)
180            .map(crate::RecoveredLedger::into_ledger)
181            .map_err(BootstrapRetirementError::Ledger)
182    }
183
184    fn validate_against<P>(
185        &mut self,
186        prior: crate::RecoveredLedger,
187        snapshot: DeclarationSnapshot,
188        policy: &P,
189        committed_at: Option<u64>,
190    ) -> Result<BootstrapCommit, BootstrapError<P::Error>>
191    where
192        P: AllocationPolicy,
193    {
194        let validated =
195            validate_allocations(&prior, snapshot, policy).map_err(BootstrapError::Validation)?;
196        let prior_ledger = prior.into_ledger();
197        let staged = prior_ledger
198            .stage_validated_generation(&validated, committed_at)
199            .map_err(BootstrapError::Staging)?;
200        let committed = self.store.commit(&staged).map_err(BootstrapError::Ledger)?;
201
202        Ok(BootstrapCommit {
203            validated: validated.with_generation(committed.current_generation()),
204            ledger: committed.into_ledger(),
205        })
206    }
207}
208
209///
210/// BootstrapCommit
211///
212/// Result of a successful generic allocation bootstrap commit.
213#[derive(Clone, Debug, Eq, PartialEq)]
214pub struct BootstrapCommit {
215    /// Ledger recovered after the protected generation commit.
216    pub ledger: AllocationLedger,
217    /// Validated allocation declarations tied to the committed generation.
218    pub validated: ValidatedAllocations,
219}
220
221///
222/// BootstrapError
223///
224/// Failure to recover, validate, or commit an allocation generation.
225#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
226pub enum BootstrapError<P> {
227    /// Ledger recovery or protected commit failed.
228    #[error(transparent)]
229    Ledger(LedgerCommitError),
230    /// Policy or historical allocation validation failed.
231    #[error(transparent)]
232    Validation(AllocationValidationError<P>),
233    /// Validated declarations could not be staged against the recovered ledger.
234    #[error(transparent)]
235    Staging(AllocationStageError),
236}
237
238///
239/// BootstrapReservationError
240///
241/// Failure to policy-check, stage, or commit an allocation reservation.
242#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
243pub enum BootstrapReservationError<P> {
244    /// Ledger recovery or protected commit failed.
245    #[error(transparent)]
246    Ledger(LedgerCommitError),
247    /// Policy adapter rejected a reservation declaration.
248    #[error("allocation policy rejected a reservation")]
249    Policy(P),
250    /// Reservation conflicted with historical allocation facts.
251    #[error(transparent)]
252    Reservation(AllocationReservationError),
253}
254
255///
256/// BootstrapRetirementError
257///
258/// Failure to stage or commit an explicit allocation retirement.
259#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
260pub enum BootstrapRetirementError {
261    /// Ledger recovery or protected commit failed.
262    #[error(transparent)]
263    Ledger(LedgerCommitError),
264    /// Retirement conflicted with historical allocation facts.
265    #[error(transparent)]
266    Retirement(AllocationRetirementError),
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use crate::{
273        declaration::AllocationDeclaration,
274        ledger::{AllocationHistory, AllocationLedger, AllocationState},
275        schema::SchemaMetadata,
276        slot::AllocationSlotDescriptor,
277    };
278
279    #[derive(Debug, Eq, PartialEq)]
280    struct TestPolicy;
281
282    impl AllocationPolicy for TestPolicy {
283        type Error = &'static str;
284
285        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
286            Ok(())
287        }
288
289        fn validate_slot(
290            &self,
291            _key: &crate::StableKey,
292            _slot: &AllocationSlotDescriptor,
293        ) -> Result<(), Self::Error> {
294            Ok(())
295        }
296
297        fn validate_reserved_slot(
298            &self,
299            _key: &crate::StableKey,
300            _slot: &AllocationSlotDescriptor,
301        ) -> Result<(), Self::Error> {
302            Ok(())
303        }
304    }
305
306    #[derive(Debug, Eq, PartialEq)]
307    struct RejectReservedPolicy;
308
309    impl AllocationPolicy for RejectReservedPolicy {
310        type Error = &'static str;
311
312        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
313            Ok(())
314        }
315
316        fn validate_slot(
317            &self,
318            _key: &crate::StableKey,
319            _slot: &AllocationSlotDescriptor,
320        ) -> Result<(), Self::Error> {
321            Ok(())
322        }
323
324        fn validate_reserved_slot(
325            &self,
326            _key: &crate::StableKey,
327            _slot: &AllocationSlotDescriptor,
328        ) -> Result<(), Self::Error> {
329            Err("reserved slot rejected")
330        }
331    }
332
333    #[derive(Debug, Eq, PartialEq)]
334    struct RejectActivePolicy;
335
336    impl AllocationPolicy for RejectActivePolicy {
337        type Error = &'static str;
338
339        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
340            Ok(())
341        }
342
343        fn validate_slot(
344            &self,
345            _key: &crate::StableKey,
346            _slot: &AllocationSlotDescriptor,
347        ) -> Result<(), Self::Error> {
348            Err("active slot rejected")
349        }
350
351        fn validate_reserved_slot(
352            &self,
353            _key: &crate::StableKey,
354            _slot: &AllocationSlotDescriptor,
355        ) -> Result<(), Self::Error> {
356            Ok(())
357        }
358    }
359
360    fn ledger() -> AllocationLedger {
361        AllocationLedger {
362            ledger_schema_version: 1,
363            physical_format_id: 1,
364            current_generation: 0,
365            allocation_history: AllocationHistory::default(),
366        }
367    }
368
369    fn declaration() -> AllocationDeclaration {
370        AllocationDeclaration::new(
371            "app.users.v1",
372            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
373            None,
374            SchemaMetadata::default(),
375        )
376        .expect("declaration")
377    }
378
379    #[test]
380    fn validate_and_commit_publishes_committed_generation() {
381        let mut store = LedgerCommitStore::default();
382        store.commit(&ledger()).expect("initial ledger");
383        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
384
385        let commit = AllocationBootstrap::new(&mut store)
386            .validate_and_commit(snapshot, &TestPolicy, Some(42))
387            .expect("bootstrap commit");
388
389        assert_eq!(commit.ledger.current_generation, 1);
390        assert_eq!(commit.validated.generation(), 1);
391        assert_eq!(commit.ledger.allocation_history.records().len(), 1);
392        assert_eq!(commit.ledger.allocation_history.generations().len(), 1);
393    }
394
395    #[test]
396    fn initialize_validate_and_commit_seeds_empty_ledger_store() {
397        let mut store = LedgerCommitStore::default();
398        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
399
400        let commit = AllocationBootstrap::new(&mut store)
401            .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
402            .expect("bootstrap commit");
403
404        assert_eq!(commit.ledger.current_generation, 1);
405        assert_eq!(commit.validated.generation(), 1);
406        assert_eq!(commit.ledger.allocation_history.records().len(), 1);
407    }
408
409    #[test]
410    fn initialize_validate_and_commit_fails_closed_on_corrupt_store() {
411        let mut store = LedgerCommitStore::default();
412        store
413            .write_corrupt_inactive_ledger(&ledger())
414            .expect("corrupt ledger");
415        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
416
417        let err = AllocationBootstrap::new(&mut store)
418            .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
419            .expect_err("corrupt state");
420
421        assert!(matches!(err, BootstrapError::Ledger(_)));
422    }
423
424    #[test]
425    fn reserve_and_commit_policy_checks_and_commits_reservation() {
426        let mut store = LedgerCommitStore::default();
427        store.commit(&ledger()).expect("initial ledger");
428        let reservation = declaration();
429
430        let committed = AllocationBootstrap::new(&mut store)
431            .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
432            .expect("reservation commit");
433
434        assert_eq!(committed.current_generation, 1);
435        assert_eq!(committed.allocation_history.records().len(), 1);
436        assert_eq!(
437            committed.allocation_history.records()[0].state(),
438            AllocationState::Reserved
439        );
440    }
441
442    #[test]
443    fn initialize_reserve_and_commit_seeds_empty_store() {
444        let mut store = LedgerCommitStore::default();
445        let reservation = declaration();
446
447        let committed = AllocationBootstrap::new(&mut store)
448            .initialize_reserve_and_commit(&ledger(), &[reservation], &TestPolicy, Some(42))
449            .expect("reservation commit");
450
451        assert_eq!(committed.current_generation, 1);
452        assert_eq!(
453            committed.allocation_history.records()[0].state(),
454            AllocationState::Reserved
455        );
456    }
457
458    #[test]
459    fn reserve_and_commit_rejects_policy_failure_before_commit() {
460        let mut store = LedgerCommitStore::default();
461        store.commit(&ledger()).expect("initial ledger");
462        let reservation = declaration();
463
464        let err = AllocationBootstrap::new(&mut store)
465            .reserve_and_commit(&[reservation], &RejectReservedPolicy, Some(42))
466            .expect_err("policy failure");
467        let recovered = store.recover().expect("recovered");
468
469        assert!(matches!(err, BootstrapReservationError::Policy(_)));
470        assert_eq!(recovered.current_generation(), 0);
471        assert!(recovered.ledger().allocation_history().records().is_empty());
472    }
473
474    #[test]
475    fn reservation_policy_alone_does_not_activate_reserved_allocation() {
476        let mut store = LedgerCommitStore::default();
477        store.commit(&ledger()).expect("initial ledger");
478        let reservation = declaration();
479        AllocationBootstrap::new(&mut store)
480            .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
481            .expect("reservation commit");
482        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
483
484        let err = AllocationBootstrap::new(&mut store)
485            .validate_and_commit(snapshot, &RejectActivePolicy, Some(43))
486            .expect_err("active validation must run");
487        let recovered = store.recover().expect("recovered");
488
489        assert!(matches!(
490            err,
491            BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
492        ));
493        assert_eq!(
494            recovered.ledger().allocation_history().records()[0].state(),
495            AllocationState::Reserved
496        );
497    }
498
499    #[test]
500    fn retire_and_commit_tombstones_through_protected_commit() {
501        let mut store = LedgerCommitStore::default();
502        store.commit(&ledger()).expect("initial ledger");
503        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
504        AllocationBootstrap::new(&mut store)
505            .validate_and_commit(snapshot, &TestPolicy, Some(42))
506            .expect("active commit");
507        let retirement = AllocationRetirement::new(
508            "app.users.v1",
509            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
510        )
511        .expect("retirement");
512
513        let committed = AllocationBootstrap::new(&mut store)
514            .retire_and_commit(&retirement, Some(43))
515            .expect("retirement commit");
516
517        assert_eq!(committed.current_generation, 2);
518        assert_eq!(
519            committed.allocation_history.records()[0].state(),
520            AllocationState::Retired
521        );
522        assert_eq!(
523            committed.allocation_history.records()[0].retired_generation(),
524            Some(2)
525        );
526    }
527
528    #[test]
529    fn retire_and_commit_rejects_unknown_key_before_commit() {
530        let mut store = LedgerCommitStore::default();
531        store.commit(&ledger()).expect("initial ledger");
532        let retirement = AllocationRetirement::new(
533            "app.users.v1",
534            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
535        )
536        .expect("retirement");
537
538        let err = AllocationBootstrap::new(&mut store)
539            .retire_and_commit(&retirement, Some(43))
540            .expect_err("unknown key");
541        let recovered = store.recover().expect("recovered");
542
543        assert!(matches!(err, BootstrapRetirementError::Retirement(_)));
544        assert_eq!(recovered.current_generation(), 0);
545        assert!(recovered.ledger().allocation_history().records().is_empty());
546    }
547}