Skip to main content

ic_memory/
bootstrap.rs

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