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