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    pub(crate) 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
263///
264/// BootstrapError
265///
266/// Failure to recover, validate, or commit an allocation generation.
267#[non_exhaustive]
268#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
269pub enum BootstrapError<P> {
270    /// Ledger recovery or protected commit failed.
271    #[error(transparent)]
272    Ledger(LedgerCommitError),
273    /// Policy or historical allocation validation failed.
274    #[error(transparent)]
275    Validation(AllocationValidationError<P>),
276    /// Validated declarations could not be staged against the recovered ledger.
277    #[error(transparent)]
278    Staging(AllocationStageError),
279}
280
281///
282/// BootstrapReservationError
283///
284/// Failure to policy-check, stage, or commit an allocation reservation.
285#[non_exhaustive]
286#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
287pub enum BootstrapReservationError<P> {
288    /// Ledger recovery or protected commit failed.
289    #[error(transparent)]
290    Ledger(LedgerCommitError),
291    /// Policy adapter rejected a reservation declaration.
292    #[error("allocation policy rejected a reservation")]
293    Policy(P),
294    /// Reservation conflicted with historical allocation facts.
295    #[error(transparent)]
296    Reservation(AllocationReservationError),
297}
298
299///
300/// BootstrapRetirementError
301///
302/// Failure to stage or commit an explicit allocation retirement.
303#[non_exhaustive]
304#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
305pub enum BootstrapRetirementError {
306    /// Ledger recovery or protected commit failed.
307    #[error(transparent)]
308    Ledger(LedgerCommitError),
309    /// Retirement conflicted with historical allocation facts.
310    #[error(transparent)]
311    Retirement(AllocationRetirementError),
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::{
318        declaration::AllocationDeclaration,
319        ledger::{AllocationHistory, AllocationLedger, AllocationState},
320        schema::SchemaMetadata,
321        slot::AllocationSlotDescriptor,
322    };
323
324    #[derive(Debug, Eq, PartialEq)]
325    struct TestPolicy;
326
327    impl AllocationPolicy for TestPolicy {
328        type Error = &'static str;
329
330        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
331            Ok(())
332        }
333
334        fn validate_slot(
335            &self,
336            _key: &crate::StableKey,
337            _slot: &AllocationSlotDescriptor,
338        ) -> Result<(), Self::Error> {
339            Ok(())
340        }
341
342        fn validate_reserved_slot(
343            &self,
344            _key: &crate::StableKey,
345            _slot: &AllocationSlotDescriptor,
346        ) -> Result<(), Self::Error> {
347            Ok(())
348        }
349    }
350
351    #[derive(Debug, Eq, PartialEq)]
352    struct RejectReservedPolicy;
353
354    impl AllocationPolicy for RejectReservedPolicy {
355        type Error = &'static str;
356
357        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
358            Ok(())
359        }
360
361        fn validate_slot(
362            &self,
363            _key: &crate::StableKey,
364            _slot: &AllocationSlotDescriptor,
365        ) -> Result<(), Self::Error> {
366            Ok(())
367        }
368
369        fn validate_reserved_slot(
370            &self,
371            _key: &crate::StableKey,
372            _slot: &AllocationSlotDescriptor,
373        ) -> Result<(), Self::Error> {
374            Err("reserved slot rejected")
375        }
376    }
377
378    #[derive(Debug, Eq, PartialEq)]
379    struct RejectActivePolicy;
380
381    impl AllocationPolicy for RejectActivePolicy {
382        type Error = &'static str;
383
384        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
385            Ok(())
386        }
387
388        fn validate_slot(
389            &self,
390            _key: &crate::StableKey,
391            _slot: &AllocationSlotDescriptor,
392        ) -> Result<(), Self::Error> {
393            Err("active slot rejected")
394        }
395
396        fn validate_reserved_slot(
397            &self,
398            _key: &crate::StableKey,
399            _slot: &AllocationSlotDescriptor,
400        ) -> Result<(), Self::Error> {
401            Ok(())
402        }
403    }
404
405    struct PolicyMustNotRun;
406
407    impl AllocationPolicy for PolicyMustNotRun {
408        type Error = &'static str;
409
410        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
411            panic!("policy received an invalid reservation")
412        }
413
414        fn validate_slot(
415            &self,
416            _key: &crate::StableKey,
417            _slot: &AllocationSlotDescriptor,
418        ) -> Result<(), Self::Error> {
419            panic!("policy received an invalid reservation")
420        }
421
422        fn validate_reserved_slot(
423            &self,
424            _key: &crate::StableKey,
425            _slot: &AllocationSlotDescriptor,
426        ) -> Result<(), Self::Error> {
427            panic!("policy received an invalid reservation")
428        }
429    }
430
431    fn ledger() -> AllocationLedger {
432        AllocationLedger {
433            current_generation: 0,
434            allocation_history: AllocationHistory::default(),
435        }
436    }
437
438    fn declaration() -> AllocationDeclaration {
439        AllocationDeclaration::new(
440            "app.users.v1",
441            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
442            None,
443            SchemaMetadata::default(),
444        )
445        .expect("declaration")
446    }
447
448    #[test]
449    fn validate_and_commit_publishes_committed_generation() {
450        let mut store = LedgerCommitStore::default();
451        store.commit(&ledger()).expect("initial ledger");
452        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
453
454        let commit = AllocationBootstrap::new(&mut store)
455            .validate_and_commit(snapshot, &TestPolicy, Some(42))
456            .expect("bootstrap commit");
457
458        assert_eq!(commit.ledger().current_generation, 1);
459        assert_eq!(commit.ledger().allocation_history.records().len(), 1);
460        assert_eq!(commit.ledger().allocation_history.generations().len(), 1);
461        assert_eq!(commit.confirm_persisted().generation(), 1);
462    }
463
464    #[test]
465    fn initialize_validate_and_commit_seeds_empty_ledger_store() {
466        let mut store = LedgerCommitStore::default();
467        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
468
469        let commit = AllocationBootstrap::new(&mut store)
470            .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
471            .expect("bootstrap commit");
472
473        assert_eq!(commit.ledger().current_generation, 1);
474        assert_eq!(commit.ledger().allocation_history.records().len(), 1);
475        assert_eq!(commit.confirm_persisted().generation(), 1);
476    }
477
478    #[test]
479    fn initialize_validate_and_commit_fails_closed_on_corrupt_store() {
480        let mut store = LedgerCommitStore::default();
481        store
482            .write_corrupt_inactive_ledger(&ledger())
483            .expect("corrupt ledger");
484        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
485
486        let err = AllocationBootstrap::new(&mut store)
487            .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
488            .expect_err("corrupt state");
489
490        assert!(matches!(err, BootstrapError::Ledger(_)));
491    }
492
493    #[test]
494    fn reserve_and_commit_policy_checks_and_commits_reservation() {
495        let mut store = LedgerCommitStore::default();
496        store.commit(&ledger()).expect("initial ledger");
497        let reservation = declaration();
498
499        let committed = AllocationBootstrap::new(&mut store)
500            .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
501            .expect("reservation commit");
502
503        assert_eq!(committed.current_generation, 1);
504        assert_eq!(committed.allocation_history.records().len(), 1);
505        assert_eq!(
506            committed.allocation_history.records()[0].state(),
507            AllocationState::Reserved
508        );
509    }
510
511    #[test]
512    fn initialize_reserve_and_commit_seeds_empty_store() {
513        let mut store = LedgerCommitStore::default();
514        let reservation = declaration();
515
516        let committed = AllocationBootstrap::new(&mut store)
517            .initialize_reserve_and_commit(&ledger(), &[reservation], &TestPolicy, Some(42))
518            .expect("reservation commit");
519
520        assert_eq!(committed.current_generation, 1);
521        assert_eq!(
522            committed.allocation_history.records()[0].state(),
523            AllocationState::Reserved
524        );
525    }
526
527    #[test]
528    fn reserve_and_commit_rejects_policy_failure_before_commit() {
529        let mut store = LedgerCommitStore::default();
530        store.commit(&ledger()).expect("initial ledger");
531        let reservation = declaration();
532
533        let err = AllocationBootstrap::new(&mut store)
534            .reserve_and_commit(&[reservation], &RejectReservedPolicy, Some(42))
535            .expect_err("policy failure");
536        let recovered = store.recover().expect("recovered");
537
538        assert!(matches!(err, BootstrapReservationError::Policy(_)));
539        assert_eq!(recovered.current_generation(), 0);
540        assert_eq!(recovered.ledger().allocation_history().records(), []);
541    }
542
543    #[test]
544    fn reserve_and_commit_validates_reservation_before_policy() {
545        let mut store = LedgerCommitStore::default();
546        store.commit(&ledger()).expect("initial ledger");
547        let mut reservation = declaration();
548        reservation.slot =
549            AllocationSlotDescriptor::memory_manager_unchecked(crate::MEMORY_MANAGER_INVALID_ID);
550
551        let err = AllocationBootstrap::new(&mut store)
552            .reserve_and_commit(&[reservation], &PolicyMustNotRun, Some(42))
553            .expect_err("invalid reservation must fail before policy");
554
555        assert!(matches!(
556            err,
557            BootstrapReservationError::Reservation(AllocationReservationError::InvalidDeclaration(
558                _
559            ))
560        ));
561    }
562
563    #[test]
564    fn reservation_policy_alone_does_not_activate_reserved_allocation() {
565        let mut store = LedgerCommitStore::default();
566        store.commit(&ledger()).expect("initial ledger");
567        let reservation = declaration();
568        AllocationBootstrap::new(&mut store)
569            .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
570            .expect("reservation commit");
571        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
572
573        let err = AllocationBootstrap::new(&mut store)
574            .validate_and_commit(snapshot, &RejectActivePolicy, Some(43))
575            .expect_err("active validation must run");
576        let recovered = store.recover().expect("recovered");
577
578        assert!(matches!(
579            err,
580            BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
581        ));
582        assert_eq!(
583            recovered.ledger().allocation_history().records()[0].state(),
584            AllocationState::Reserved
585        );
586    }
587
588    #[test]
589    fn retire_and_commit_tombstones_through_protected_commit() {
590        let mut store = LedgerCommitStore::default();
591        store.commit(&ledger()).expect("initial ledger");
592        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
593        AllocationBootstrap::new(&mut store)
594            .validate_and_commit(snapshot, &TestPolicy, Some(42))
595            .expect("active commit");
596        let retirement = AllocationRetirement::new(
597            "app.users.v1",
598            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
599        )
600        .expect("retirement");
601
602        let committed = AllocationBootstrap::new(&mut store)
603            .retire_and_commit(&retirement, Some(43))
604            .expect("retirement commit");
605
606        assert_eq!(committed.current_generation, 2);
607        assert_eq!(
608            committed.allocation_history.records()[0].state(),
609            AllocationState::Retired { generation: 2 }
610        );
611    }
612
613    #[test]
614    fn retire_and_commit_rejects_unknown_key_before_commit() {
615        let mut store = LedgerCommitStore::default();
616        store.commit(&ledger()).expect("initial ledger");
617        let retirement = AllocationRetirement::new(
618            "app.users.v1",
619            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
620        )
621        .expect("retirement");
622
623        let err = AllocationBootstrap::new(&mut store)
624            .retire_and_commit(&retirement, Some(43))
625            .expect_err("unknown key");
626        let recovered = store.recover().expect("recovered");
627
628        assert!(matches!(err, BootstrapRetirementError::Retirement(_)));
629        assert_eq!(recovered.current_generation(), 0);
630        assert_eq!(recovered.ledger().allocation_history().records(), []);
631    }
632}