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