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        checked_reservation_count, stage_reservation_generation, stage_retirement_generation,
9        stage_validated_generation,
10    },
11    policy::AllocationPolicy,
12    validation::{AllocationValidationError, validate_allocations},
13};
14use std::borrow::Cow;
15
16///
17/// AllocationBootstrap
18///
19/// Golden-path allocation ledger bootstrap pipeline.
20///
21/// This type owns allocation-governance sequencing only: recover the persisted
22/// ledger, apply the owner layer's policy, validate current declarations
23/// against ledger history, stage and commit the next generation, and return
24/// a pending [`PendingBootstrapCommit`] after the in-memory commit store advances.
25/// The persistence owner must durably write that state and explicitly confirm
26/// persistence before it can obtain [`CommittedAllocations`].
27///
28/// `AllocationBootstrap` is for whichever layer owns a given `ic-memory`
29/// ledger store. That owner may be a framework such as Canic, a library such as
30/// IcyDB using `ic-memory` directly, or a standalone application canister. The
31/// ownership model is not a fixed `ic-memory -> Canic -> IcyDB -> application`
32/// chain.
33///
34/// Exactly one owner should bootstrap a given ledger store. If multiple layers
35/// use `ic-memory` in the same canister, they must either compose their
36/// declarations into one bootstrap owner or use distinct ledger stores and
37/// allocation domains.
38///
39/// The owner still decides when bootstrap runs, how the ledger store is backed
40/// by stable memory, and when endpoint dispatch or stable-memory handle opening
41/// is allowed.
42#[derive(Debug)]
43pub struct AllocationBootstrap<'store> {
44    store: &'store mut LedgerCommitStore,
45}
46
47impl<'store> AllocationBootstrap<'store> {
48    /// Build a bootstrap pipeline over a protected ledger commit store.
49    pub const fn new(store: &'store mut LedgerCommitStore) -> Self {
50        Self { store }
51    }
52
53    /// Recover, validate, stage, and advance one pending allocation generation.
54    pub fn validate_and_commit<P>(
55        &mut self,
56        snapshot: DeclarationSnapshot,
57        policy: &P,
58        committed_at: Option<u64>,
59    ) -> Result<PendingBootstrapCommit, BootstrapError<P::Error>>
60    where
61        P: AllocationPolicy,
62    {
63        let prior = self.store.recover().map_err(BootstrapError::Ledger)?;
64        self.validate_against(prior, snapshot, policy, committed_at)
65    }
66
67    /// Initialize an empty ledger store, then validate and advance a pending commit.
68    ///
69    /// This is the privileged genesis/import path. Normal runtime users should
70    /// use [`crate::MemoryRuntime::bootstrap`] directly or the default TLS
71    /// convenience bootstrap, both of which supply an empty current-format
72    /// genesis ledger. A non-empty `genesis` should only be supplied by the layer
73    /// that owns migration or import for this ledger store.
74    ///
75    /// The generic crate guarantees only that `genesis` is used when the
76    /// protected physical store is empty, never when recovery sees corrupt or
77    /// partially written state.
78    pub fn initialize_validate_and_commit<P>(
79        &mut self,
80        genesis: &AllocationLedger,
81        snapshot: DeclarationSnapshot,
82        policy: &P,
83        committed_at: Option<u64>,
84    ) -> Result<PendingBootstrapCommit, BootstrapError<P::Error>>
85    where
86        P: AllocationPolicy,
87    {
88        let prior = self
89            .store
90            .recover_or_initialize(genesis)
91            .map_err(BootstrapError::Ledger)?;
92        self.validate_against(prior, snapshot, policy, committed_at)
93    }
94
95    /// Recover, policy-check, reserve, and commit one reservation generation.
96    ///
97    /// Declarations carry checked metadata. After recovery, batches exceeding
98    /// 255 items reject before policy callbacks or historical claim checks.
99    pub fn reserve_and_commit<P>(
100        &mut self,
101        reservations: &[AllocationDeclaration],
102        policy: &P,
103        committed_at: Option<u64>,
104    ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
105    where
106        P: AllocationPolicy,
107    {
108        let prior = self
109            .store
110            .recover()
111            .map_err(BootstrapReservationError::Ledger)?;
112        self.reserve_against(prior.into_ledger(), reservations, policy, committed_at)
113    }
114
115    /// Initialize an empty ledger store, then reserve and commit.
116    ///
117    /// This is the privileged genesis/import path for reservation staging. A
118    /// non-empty `genesis` should only be supplied by the owner of migration or
119    /// import for this ledger store.
120    /// Recovery or initialization precedes batch validation. Batches exceeding
121    /// 255 items reject before policy callbacks or historical claim checks.
122    pub fn initialize_reserve_and_commit<P>(
123        &mut self,
124        genesis: &AllocationLedger,
125        reservations: &[AllocationDeclaration],
126        policy: &P,
127        committed_at: Option<u64>,
128    ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
129    where
130        P: AllocationPolicy,
131    {
132        let prior = self
133            .store
134            .recover_or_initialize(genesis)
135            .map_err(BootstrapReservationError::Ledger)?;
136        self.reserve_against(prior.into_ledger(), reservations, policy, committed_at)
137    }
138
139    /// Recover, retire, and commit one explicit retirement generation.
140    pub fn retire_and_commit(
141        &mut self,
142        retirement: &AllocationRetirement,
143        committed_at: Option<u64>,
144    ) -> Result<AllocationLedger, BootstrapRetirementError> {
145        let prior = self
146            .store
147            .recover()
148            .map_err(BootstrapRetirementError::Ledger)?;
149        let staged =
150            stage_retirement_generation(Cow::Owned(prior.into_ledger()), retirement, committed_at)
151                .map_err(BootstrapRetirementError::Retirement)?;
152        self.store
153            .commit_generation(&staged)
154            .map_err(BootstrapRetirementError::Ledger)?;
155        Ok(staged)
156    }
157
158    fn reserve_against<P>(
159        &mut self,
160        prior: AllocationLedger,
161        reservations: &[AllocationDeclaration],
162        policy: &P,
163        committed_at: Option<u64>,
164    ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
165    where
166        P: AllocationPolicy,
167    {
168        checked_reservation_count(reservations.len())
169            .map_err(BootstrapReservationError::Reservation)?;
170        for reservation in reservations {
171            policy
172                .validate_key(&reservation.stable_key)
173                .map_err(BootstrapReservationError::Policy)?;
174            policy
175                .validate_reserved_slot(&reservation.stable_key, &reservation.slot)
176                .map_err(BootstrapReservationError::Policy)?;
177        }
178
179        let staged = stage_reservation_generation(Cow::Owned(prior), reservations, committed_at)
180            .map_err(BootstrapReservationError::Reservation)?;
181        self.store
182            .commit_generation(&staged)
183            .map_err(BootstrapReservationError::Ledger)?;
184        Ok(staged)
185    }
186
187    pub(crate) fn validate_against<P>(
188        &mut self,
189        prior: crate::RecoveredLedger,
190        snapshot: DeclarationSnapshot,
191        policy: &P,
192        committed_at: Option<u64>,
193    ) -> Result<PendingBootstrapCommit, BootstrapError<P::Error>>
194    where
195        P: AllocationPolicy,
196    {
197        let validated =
198            validate_allocations(&prior, snapshot, policy).map_err(BootstrapError::Validation)?;
199        let staged =
200            stage_validated_generation(Cow::Owned(prior.into_ledger()), &validated, committed_at)
201                .map_err(BootstrapError::Staging)?;
202        self.store
203            .commit_generation(&staged)
204            .map_err(BootstrapError::Ledger)?;
205
206        Ok(PendingBootstrapCommit {
207            validated,
208            ledger: staged,
209        })
210    }
211}
212
213///
214/// PendingBootstrapCommit
215///
216/// Pending result of a successful generic allocation bootstrap commit.
217///
218/// The embedded [`crate::LedgerCommitStore`] has advanced, but this generic
219/// layer does not own stable-memory IO. Persist the owning record first, then
220/// call [`PendingBootstrapCommit::confirm_persisted`] to mint the allocation-open
221/// capability.
222///
223
224#[derive(Debug, Eq, PartialEq)]
225#[must_use = "persist the owning ledger record, then confirm_persisted before opening allocations"]
226pub struct PendingBootstrapCommit {
227    /// Staged ledger accepted by 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
262///
263/// BootstrapError
264///
265/// Failure to recover, validate, or commit an allocation generation.
266#[non_exhaustive]
267#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
268pub enum BootstrapError<P> {
269    /// Ledger recovery or protected commit failed.
270    #[error(transparent)]
271    Ledger(LedgerCommitError),
272    /// Policy or historical allocation validation failed.
273    #[error(transparent)]
274    Validation(AllocationValidationError<P>),
275    /// Validated declarations could not be staged against the recovered ledger.
276    #[error(transparent)]
277    Staging(AllocationStageError),
278}
279
280///
281/// BootstrapReservationError
282///
283/// Failure to policy-check, stage, or commit an allocation reservation.
284#[non_exhaustive]
285#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
286pub enum BootstrapReservationError<P> {
287    /// Ledger recovery or protected commit failed.
288    #[error(transparent)]
289    Ledger(LedgerCommitError),
290    /// Policy adapter rejected a reservation declaration.
291    #[error("allocation policy rejected a reservation")]
292    Policy(P),
293    /// Reservation conflicted with historical allocation facts.
294    #[error(transparent)]
295    Reservation(AllocationReservationError),
296}
297
298///
299/// BootstrapRetirementError
300///
301/// Failure to stage or commit an explicit allocation retirement.
302#[non_exhaustive]
303#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
304pub enum BootstrapRetirementError {
305    /// Ledger recovery or protected commit failed.
306    #[error(transparent)]
307    Ledger(LedgerCommitError),
308    /// Retirement conflicted with historical allocation facts.
309    #[error(transparent)]
310    Retirement(AllocationRetirementError),
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use crate::{
317        declaration::AllocationDeclaration,
318        ledger::{AllocationHistory, AllocationLedger, AllocationState},
319        schema::SchemaMetadata,
320        slot::MemoryManagerSlot,
321    };
322
323    #[derive(Debug, Eq, PartialEq)]
324    struct TestPolicy;
325
326    impl AllocationPolicy for TestPolicy {
327        type Error = &'static str;
328
329        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
330            Ok(())
331        }
332
333        fn validate_slot(
334            &self,
335            _key: &crate::StableKey,
336            _slot: &MemoryManagerSlot,
337        ) -> Result<(), Self::Error> {
338            Ok(())
339        }
340
341        fn validate_reserved_slot(
342            &self,
343            _key: &crate::StableKey,
344            _slot: &MemoryManagerSlot,
345        ) -> Result<(), Self::Error> {
346            Ok(())
347        }
348    }
349
350    #[derive(Debug, Eq, PartialEq)]
351    struct RejectReservedPolicy;
352
353    impl AllocationPolicy for RejectReservedPolicy {
354        type Error = &'static str;
355
356        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
357            Ok(())
358        }
359
360        fn validate_slot(
361            &self,
362            _key: &crate::StableKey,
363            _slot: &MemoryManagerSlot,
364        ) -> Result<(), Self::Error> {
365            Ok(())
366        }
367
368        fn validate_reserved_slot(
369            &self,
370            _key: &crate::StableKey,
371            _slot: &MemoryManagerSlot,
372        ) -> Result<(), Self::Error> {
373            Err("reserved slot rejected")
374        }
375    }
376
377    #[derive(Debug, Eq, PartialEq)]
378    struct RejectActivePolicy;
379
380    impl AllocationPolicy for RejectActivePolicy {
381        type Error = &'static str;
382
383        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
384            Ok(())
385        }
386
387        fn validate_slot(
388            &self,
389            _key: &crate::StableKey,
390            _slot: &MemoryManagerSlot,
391        ) -> Result<(), Self::Error> {
392            Err("active slot rejected")
393        }
394
395        fn validate_reserved_slot(
396            &self,
397            _key: &crate::StableKey,
398            _slot: &MemoryManagerSlot,
399        ) -> Result<(), Self::Error> {
400            Ok(())
401        }
402    }
403
404    struct PolicyMustNotRun;
405
406    impl AllocationPolicy for PolicyMustNotRun {
407        type Error = &'static str;
408
409        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
410            panic!("policy received an invalid reservation")
411        }
412
413        fn validate_slot(
414            &self,
415            _key: &crate::StableKey,
416            _slot: &MemoryManagerSlot,
417        ) -> Result<(), Self::Error> {
418            panic!("policy received an invalid reservation")
419        }
420
421        fn validate_reserved_slot(
422            &self,
423            _key: &crate::StableKey,
424            _slot: &MemoryManagerSlot,
425        ) -> Result<(), Self::Error> {
426            panic!("policy received an invalid reservation")
427        }
428    }
429
430    fn ledger() -> AllocationLedger {
431        AllocationLedger {
432            current_generation: 0,
433            allocation_history: AllocationHistory::default(),
434        }
435    }
436
437    fn declaration() -> AllocationDeclaration {
438        AllocationDeclaration::new(
439            "app.users.v1",
440            MemoryManagerSlot::new(100).expect("usable slot"),
441            None,
442            SchemaMetadata::default(),
443        )
444        .expect("declaration")
445    }
446
447    #[test]
448    fn validate_and_commit_publishes_committed_generation() {
449        let mut store = LedgerCommitStore::default();
450        store.commit(&ledger()).expect("initial ledger");
451        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
452
453        let commit = AllocationBootstrap::new(&mut store)
454            .validate_and_commit(snapshot, &TestPolicy, Some(42))
455            .expect("bootstrap commit");
456
457        assert_eq!(commit.ledger().current_generation, 1);
458        assert_eq!(commit.ledger().allocation_history.records().len(), 1);
459        assert_eq!(commit.ledger().allocation_history.generations().len(), 1);
460        assert_eq!(store.recover().unwrap().ledger(), commit.ledger());
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        assert_eq!(store.recover().unwrap().ledger(), &committed);
510
511        // The first reservation changes local staging state before the second
512        // tries to move an existing key. Neither borrowed staging nor bootstrap
513        // may expose that partial batch or advance the protected store.
514        let reservations = [
515            AllocationDeclaration::memory_manager_unlabeled("app.future.v1", 101).unwrap(),
516            AllocationDeclaration::memory_manager_unlabeled("app.users.v1", 102).unwrap(),
517        ];
518        let before = store.clone();
519        let expected = committed
520            .stage_reservation_generation(&reservations, None)
521            .unwrap_err();
522        assert!(matches!(
523            expected,
524            AllocationReservationError::StableKeySlotConflict { .. }
525        ));
526        assert_eq!(committed, *store.recover().unwrap().ledger());
527        let error = AllocationBootstrap::new(&mut store)
528            .reserve_and_commit(&reservations, &TestPolicy, None)
529            .unwrap_err();
530        assert_eq!(error, BootstrapReservationError::Reservation(expected));
531        assert_eq!(store, before);
532    }
533
534    #[test]
535    fn initialize_reserve_and_commit_seeds_empty_store() {
536        let mut store = LedgerCommitStore::default();
537        let reservation = declaration();
538
539        let committed = AllocationBootstrap::new(&mut store)
540            .initialize_reserve_and_commit(&ledger(), &[reservation], &TestPolicy, Some(42))
541            .expect("reservation commit");
542
543        assert_eq!(committed.current_generation, 1);
544        assert_eq!(
545            committed.allocation_history.records()[0].state(),
546            AllocationState::Reserved
547        );
548    }
549
550    #[test]
551    fn reserve_and_commit_rejects_policy_failure_before_commit() {
552        let mut store = LedgerCommitStore::default();
553        store.commit(&ledger()).expect("initial ledger");
554        let reservation = declaration();
555
556        let err = AllocationBootstrap::new(&mut store)
557            .reserve_and_commit(&[reservation], &RejectReservedPolicy, Some(42))
558            .expect_err("policy failure");
559        let recovered = store.recover().expect("recovered");
560
561        assert!(matches!(err, BootstrapReservationError::Policy(_)));
562        assert_eq!(recovered.current_generation(), 0);
563        assert_eq!(recovered.ledger().allocation_history().records(), []);
564    }
565
566    #[test]
567    fn reservation_pipeline_accepts_empty_and_full_slot_domain_batches() {
568        for count in [0_u8, 255] {
569            let reservations = (0..count)
570                .map(|id| {
571                    AllocationDeclaration::memory_manager_unlabeled(
572                        format!("app.future{id}.v1"),
573                        id,
574                    )
575                    .expect("reservation")
576                })
577                .collect::<Vec<_>>();
578            let mut store = LedgerCommitStore::default();
579            store.commit(&ledger()).expect("initial ledger");
580
581            let committed = AllocationBootstrap::new(&mut store)
582                .reserve_and_commit(&reservations, &TestPolicy, Some(42))
583                .expect("bounded batch commits");
584
585            assert_eq!(committed.current_generation(), 1);
586            assert_eq!(
587                committed.allocation_history().records().len(),
588                usize::from(count)
589            );
590            assert_eq!(
591                committed.allocation_history().generations()[0].declaration_count(),
592                u32::from(count)
593            );
594            assert_eq!(store.recover().unwrap().ledger(), &committed);
595        }
596    }
597
598    #[test]
599    fn oversized_reservations_reject_before_policy_and_preserve_existing_store() {
600        let reservations = vec![declaration(); 256];
601        for initialize in [false, true] {
602            let mut store = LedgerCommitStore::default();
603            store.commit(&ledger()).expect("initial ledger");
604            let before = store.clone();
605            let mut bootstrap = AllocationBootstrap::new(&mut store);
606            let error = if initialize {
607                bootstrap.initialize_reserve_and_commit(
608                    &ledger(),
609                    &reservations,
610                    &PolicyMustNotRun,
611                    None,
612                )
613            } else {
614                bootstrap.reserve_and_commit(&reservations, &PolicyMustNotRun, None)
615            }
616            .expect_err("oversized batch must fail before policy");
617
618            assert_eq!(
619                error,
620                BootstrapReservationError::Reservation(
621                    AllocationReservationError::TooManyReservations { count: 256 }
622                )
623            );
624            assert_eq!(store, before);
625        }
626    }
627
628    #[test]
629    fn oversized_initial_reservations_preserve_genesis_before_policy() {
630        let reservations = vec![declaration(); 256];
631        let mut store = LedgerCommitStore::default();
632        let mut expected = LedgerCommitStore::default();
633        expected.commit(&ledger()).expect("expected genesis");
634
635        let error = AllocationBootstrap::new(&mut store)
636            .initialize_reserve_and_commit(&ledger(), &reservations, &PolicyMustNotRun, None)
637            .expect_err("size rejection precedes policy checks");
638
639        assert_eq!(
640            error,
641            BootstrapReservationError::Reservation(
642                AllocationReservationError::TooManyReservations { count: 256 }
643            )
644        );
645        assert_eq!(store, expected);
646    }
647
648    #[test]
649    fn reservation_policy_alone_does_not_activate_reserved_allocation() {
650        let mut store = LedgerCommitStore::default();
651        store.commit(&ledger()).expect("initial ledger");
652        let reservation = declaration();
653        AllocationBootstrap::new(&mut store)
654            .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
655            .expect("reservation commit");
656        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
657
658        let err = AllocationBootstrap::new(&mut store)
659            .validate_and_commit(snapshot, &RejectActivePolicy, Some(43))
660            .expect_err("active validation must run");
661        let recovered = store.recover().expect("recovered");
662
663        assert!(matches!(
664            err,
665            BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
666        ));
667        assert_eq!(
668            recovered.ledger().allocation_history().records()[0].state(),
669            AllocationState::Reserved
670        );
671    }
672
673    #[test]
674    fn retire_and_commit_tombstones_through_protected_commit() {
675        let mut store = LedgerCommitStore::default();
676        store.commit(&ledger()).expect("initial ledger");
677        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
678        let _pending = AllocationBootstrap::new(&mut store)
679            .validate_and_commit(snapshot, &TestPolicy, Some(42))
680            .expect("active commit");
681        let retirement = AllocationRetirement::new(
682            "app.users.v1",
683            MemoryManagerSlot::new(100).expect("usable slot"),
684        )
685        .expect("retirement");
686
687        let committed = AllocationBootstrap::new(&mut store)
688            .retire_and_commit(&retirement, Some(43))
689            .expect("retirement commit");
690
691        assert_eq!(committed.current_generation, 2);
692        assert_eq!(
693            committed.allocation_history.records()[0].state(),
694            AllocationState::Retired { generation: 2 }
695        );
696        assert_eq!(store.recover().unwrap().ledger(), &committed);
697    }
698
699    #[test]
700    fn retire_and_commit_rejects_unknown_key_before_commit() {
701        let mut store = LedgerCommitStore::default();
702        store.commit(&ledger()).expect("initial ledger");
703        let retirement = AllocationRetirement::new(
704            "app.users.v1",
705            MemoryManagerSlot::new(100).expect("usable slot"),
706        )
707        .expect("retirement");
708
709        let err = AllocationBootstrap::new(&mut store)
710            .retire_and_commit(&retirement, Some(43))
711            .expect_err("unknown key");
712        let recovered = store.recover().expect("recovered");
713
714        assert!(matches!(err, BootstrapRetirementError::Retirement(_)));
715        assert_eq!(recovered.current_generation(), 0);
716        assert_eq!(recovered.ledger().allocation_history().records(), []);
717    }
718}