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)]
225pub struct PendingBootstrapCommit {
226    /// Staged ledger accepted by the protected generation commit.
227    ledger: AllocationLedger,
228    /// Validated allocation declarations awaiting persistence confirmation.
229    validated: ValidatedAllocations,
230}
231
232impl PendingBootstrapCommit {
233    /// Borrow the committed logical ledger for diagnostics.
234    ///
235    /// The persistence owner must write the owning record that contains the
236    /// mutated [`crate::LedgerCommitStore`], not serialize this ledger DTO as a
237    /// replacement protocol.
238    #[must_use]
239    pub const fn ledger(&self) -> &AllocationLedger {
240        &self.ledger
241    }
242
243    /// Borrow the pre-commit validation result for diagnostics.
244    #[must_use]
245    pub const fn validated(&self) -> &ValidatedAllocations {
246        &self.validated
247    }
248
249    /// Confirm that the owning integration durably persisted this commit.
250    ///
251    /// Calling this method before the stable-memory write succeeds violates the
252    /// allocation protocol. The default runtime performs its stable-cell write
253    /// before confirmation.
254    #[must_use]
255    pub fn confirm_persisted(self) -> CommittedAllocations {
256        self.validated
257            .confirm_persisted(self.ledger.current_generation())
258    }
259}
260
261///
262/// BootstrapError
263///
264/// Failure to recover, validate, or commit an allocation generation.
265#[non_exhaustive]
266#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
267pub enum BootstrapError<P> {
268    /// Ledger recovery or protected commit failed.
269    #[error(transparent)]
270    Ledger(LedgerCommitError),
271    /// Policy or historical allocation validation failed.
272    #[error(transparent)]
273    Validation(AllocationValidationError<P>),
274    /// Validated declarations could not be staged against the recovered ledger.
275    #[error(transparent)]
276    Staging(AllocationStageError),
277}
278
279///
280/// BootstrapReservationError
281///
282/// Failure to policy-check, stage, or commit an allocation reservation.
283#[non_exhaustive]
284#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
285pub enum BootstrapReservationError<P> {
286    /// Ledger recovery or protected commit failed.
287    #[error(transparent)]
288    Ledger(LedgerCommitError),
289    /// Policy adapter rejected a reservation declaration.
290    #[error("allocation policy rejected a reservation")]
291    Policy(P),
292    /// Reservation conflicted with historical allocation facts.
293    #[error(transparent)]
294    Reservation(AllocationReservationError),
295}
296
297///
298/// BootstrapRetirementError
299///
300/// Failure to stage or commit an explicit allocation retirement.
301#[non_exhaustive]
302#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
303pub enum BootstrapRetirementError {
304    /// Ledger recovery or protected commit failed.
305    #[error(transparent)]
306    Ledger(LedgerCommitError),
307    /// Retirement conflicted with historical allocation facts.
308    #[error(transparent)]
309    Retirement(AllocationRetirementError),
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use crate::{
316        declaration::AllocationDeclaration,
317        ledger::{AllocationHistory, AllocationLedger, AllocationState},
318        schema::SchemaMetadata,
319        slot::MemoryManagerSlot,
320    };
321
322    #[derive(Debug, Eq, PartialEq)]
323    struct TestPolicy;
324
325    impl AllocationPolicy for TestPolicy {
326        type Error = &'static str;
327
328        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
329            Ok(())
330        }
331
332        fn validate_slot(
333            &self,
334            _key: &crate::StableKey,
335            _slot: &MemoryManagerSlot,
336        ) -> Result<(), Self::Error> {
337            Ok(())
338        }
339
340        fn validate_reserved_slot(
341            &self,
342            _key: &crate::StableKey,
343            _slot: &MemoryManagerSlot,
344        ) -> Result<(), Self::Error> {
345            Ok(())
346        }
347    }
348
349    #[derive(Debug, Eq, PartialEq)]
350    struct RejectReservedPolicy;
351
352    impl AllocationPolicy for RejectReservedPolicy {
353        type Error = &'static str;
354
355        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
356            Ok(())
357        }
358
359        fn validate_slot(
360            &self,
361            _key: &crate::StableKey,
362            _slot: &MemoryManagerSlot,
363        ) -> Result<(), Self::Error> {
364            Ok(())
365        }
366
367        fn validate_reserved_slot(
368            &self,
369            _key: &crate::StableKey,
370            _slot: &MemoryManagerSlot,
371        ) -> Result<(), Self::Error> {
372            Err("reserved slot rejected")
373        }
374    }
375
376    #[derive(Debug, Eq, PartialEq)]
377    struct RejectActivePolicy;
378
379    impl AllocationPolicy for RejectActivePolicy {
380        type Error = &'static str;
381
382        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
383            Ok(())
384        }
385
386        fn validate_slot(
387            &self,
388            _key: &crate::StableKey,
389            _slot: &MemoryManagerSlot,
390        ) -> Result<(), Self::Error> {
391            Err("active slot rejected")
392        }
393
394        fn validate_reserved_slot(
395            &self,
396            _key: &crate::StableKey,
397            _slot: &MemoryManagerSlot,
398        ) -> Result<(), Self::Error> {
399            Ok(())
400        }
401    }
402
403    struct PolicyMustNotRun;
404
405    impl AllocationPolicy for PolicyMustNotRun {
406        type Error = &'static str;
407
408        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
409            panic!("policy received an invalid reservation")
410        }
411
412        fn validate_slot(
413            &self,
414            _key: &crate::StableKey,
415            _slot: &MemoryManagerSlot,
416        ) -> Result<(), Self::Error> {
417            panic!("policy received an invalid reservation")
418        }
419
420        fn validate_reserved_slot(
421            &self,
422            _key: &crate::StableKey,
423            _slot: &MemoryManagerSlot,
424        ) -> Result<(), Self::Error> {
425            panic!("policy received an invalid reservation")
426        }
427    }
428
429    fn ledger() -> AllocationLedger {
430        AllocationLedger {
431            current_generation: 0,
432            allocation_history: AllocationHistory::default(),
433        }
434    }
435
436    fn declaration() -> AllocationDeclaration {
437        AllocationDeclaration::new(
438            "app.users.v1",
439            MemoryManagerSlot::new(100).expect("usable slot"),
440            None,
441            SchemaMetadata::default(),
442        )
443        .expect("declaration")
444    }
445
446    #[test]
447    fn validate_and_commit_publishes_committed_generation() {
448        let mut store = LedgerCommitStore::default();
449        store.commit(&ledger()).expect("initial ledger");
450        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
451
452        let commit = AllocationBootstrap::new(&mut store)
453            .validate_and_commit(snapshot, &TestPolicy, Some(42))
454            .expect("bootstrap commit");
455
456        assert_eq!(commit.ledger().current_generation, 1);
457        assert_eq!(commit.ledger().allocation_history.records().len(), 1);
458        assert_eq!(commit.ledger().allocation_history.generations().len(), 1);
459        assert_eq!(store.recover().unwrap().ledger(), commit.ledger());
460        assert_eq!(commit.confirm_persisted().generation(), 1);
461    }
462
463    #[test]
464    fn initialize_validate_and_commit_seeds_empty_ledger_store() {
465        let mut store = LedgerCommitStore::default();
466        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
467
468        let commit = AllocationBootstrap::new(&mut store)
469            .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
470            .expect("bootstrap commit");
471
472        assert_eq!(commit.ledger().current_generation, 1);
473        assert_eq!(commit.ledger().allocation_history.records().len(), 1);
474        assert_eq!(commit.confirm_persisted().generation(), 1);
475    }
476
477    #[test]
478    fn initialize_validate_and_commit_fails_closed_on_corrupt_store() {
479        let mut store = LedgerCommitStore::default();
480        store
481            .write_corrupt_inactive_ledger(&ledger())
482            .expect("corrupt ledger");
483        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
484
485        let err = AllocationBootstrap::new(&mut store)
486            .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
487            .expect_err("corrupt state");
488
489        assert!(matches!(err, BootstrapError::Ledger(_)));
490    }
491
492    #[test]
493    fn reserve_and_commit_policy_checks_and_commits_reservation() {
494        let mut store = LedgerCommitStore::default();
495        store.commit(&ledger()).expect("initial ledger");
496        let reservation = declaration();
497
498        let committed = AllocationBootstrap::new(&mut store)
499            .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
500            .expect("reservation commit");
501
502        assert_eq!(committed.current_generation, 1);
503        assert_eq!(committed.allocation_history.records().len(), 1);
504        assert_eq!(
505            committed.allocation_history.records()[0].state(),
506            AllocationState::Reserved
507        );
508        assert_eq!(store.recover().unwrap().ledger(), &committed);
509
510        // The first reservation changes local staging state before the second
511        // tries to move an existing key. Neither borrowed staging nor bootstrap
512        // may expose that partial batch or advance the protected store.
513        let reservations = [
514            AllocationDeclaration::memory_manager_unlabeled("app.future.v1", 101).unwrap(),
515            AllocationDeclaration::memory_manager_unlabeled("app.users.v1", 102).unwrap(),
516        ];
517        let before = store.clone();
518        let expected = committed
519            .stage_reservation_generation(&reservations, None)
520            .unwrap_err();
521        assert!(matches!(
522            expected,
523            AllocationReservationError::StableKeySlotConflict { .. }
524        ));
525        assert_eq!(committed, *store.recover().unwrap().ledger());
526        let error = AllocationBootstrap::new(&mut store)
527            .reserve_and_commit(&reservations, &TestPolicy, None)
528            .unwrap_err();
529        assert_eq!(error, BootstrapReservationError::Reservation(expected));
530        assert_eq!(store, before);
531    }
532
533    #[test]
534    fn initialize_reserve_and_commit_seeds_empty_store() {
535        let mut store = LedgerCommitStore::default();
536        let reservation = declaration();
537
538        let committed = AllocationBootstrap::new(&mut store)
539            .initialize_reserve_and_commit(&ledger(), &[reservation], &TestPolicy, Some(42))
540            .expect("reservation commit");
541
542        assert_eq!(committed.current_generation, 1);
543        assert_eq!(
544            committed.allocation_history.records()[0].state(),
545            AllocationState::Reserved
546        );
547    }
548
549    #[test]
550    fn reserve_and_commit_rejects_policy_failure_before_commit() {
551        let mut store = LedgerCommitStore::default();
552        store.commit(&ledger()).expect("initial ledger");
553        let reservation = declaration();
554
555        let err = AllocationBootstrap::new(&mut store)
556            .reserve_and_commit(&[reservation], &RejectReservedPolicy, Some(42))
557            .expect_err("policy failure");
558        let recovered = store.recover().expect("recovered");
559
560        assert!(matches!(err, BootstrapReservationError::Policy(_)));
561        assert_eq!(recovered.current_generation(), 0);
562        assert_eq!(recovered.ledger().allocation_history().records(), []);
563    }
564
565    #[test]
566    fn reservation_pipeline_accepts_empty_and_full_slot_domain_batches() {
567        for count in [0_u8, 255] {
568            let reservations = (0..count)
569                .map(|id| {
570                    AllocationDeclaration::memory_manager_unlabeled(
571                        format!("app.future{id}.v1"),
572                        id,
573                    )
574                    .expect("reservation")
575                })
576                .collect::<Vec<_>>();
577            let mut store = LedgerCommitStore::default();
578            store.commit(&ledger()).expect("initial ledger");
579
580            let committed = AllocationBootstrap::new(&mut store)
581                .reserve_and_commit(&reservations, &TestPolicy, Some(42))
582                .expect("bounded batch commits");
583
584            assert_eq!(committed.current_generation(), 1);
585            assert_eq!(
586                committed.allocation_history().records().len(),
587                usize::from(count)
588            );
589            assert_eq!(
590                committed.allocation_history().generations()[0].declaration_count(),
591                u32::from(count)
592            );
593            assert_eq!(store.recover().unwrap().ledger(), &committed);
594        }
595    }
596
597    #[test]
598    fn oversized_reservations_reject_before_policy_and_preserve_existing_store() {
599        let reservations = vec![declaration(); 256];
600        for initialize in [false, true] {
601            let mut store = LedgerCommitStore::default();
602            store.commit(&ledger()).expect("initial ledger");
603            let before = store.clone();
604            let mut bootstrap = AllocationBootstrap::new(&mut store);
605            let error = if initialize {
606                bootstrap.initialize_reserve_and_commit(
607                    &ledger(),
608                    &reservations,
609                    &PolicyMustNotRun,
610                    None,
611                )
612            } else {
613                bootstrap.reserve_and_commit(&reservations, &PolicyMustNotRun, None)
614            }
615            .expect_err("oversized batch must fail before policy");
616
617            assert_eq!(
618                error,
619                BootstrapReservationError::Reservation(
620                    AllocationReservationError::TooManyReservations { count: 256 }
621                )
622            );
623            assert_eq!(store, before);
624        }
625    }
626
627    #[test]
628    fn oversized_initial_reservations_preserve_genesis_before_policy() {
629        let reservations = vec![declaration(); 256];
630        let mut store = LedgerCommitStore::default();
631        let mut expected = LedgerCommitStore::default();
632        expected.commit(&ledger()).expect("expected genesis");
633
634        let error = AllocationBootstrap::new(&mut store)
635            .initialize_reserve_and_commit(&ledger(), &reservations, &PolicyMustNotRun, None)
636            .expect_err("size rejection precedes policy checks");
637
638        assert_eq!(
639            error,
640            BootstrapReservationError::Reservation(
641                AllocationReservationError::TooManyReservations { count: 256 }
642            )
643        );
644        assert_eq!(store, expected);
645    }
646
647    #[test]
648    fn reservation_policy_alone_does_not_activate_reserved_allocation() {
649        let mut store = LedgerCommitStore::default();
650        store.commit(&ledger()).expect("initial ledger");
651        let reservation = declaration();
652        AllocationBootstrap::new(&mut store)
653            .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
654            .expect("reservation commit");
655        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
656
657        let err = AllocationBootstrap::new(&mut store)
658            .validate_and_commit(snapshot, &RejectActivePolicy, Some(43))
659            .expect_err("active validation must run");
660        let recovered = store.recover().expect("recovered");
661
662        assert!(matches!(
663            err,
664            BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
665        ));
666        assert_eq!(
667            recovered.ledger().allocation_history().records()[0].state(),
668            AllocationState::Reserved
669        );
670    }
671
672    #[test]
673    fn retire_and_commit_tombstones_through_protected_commit() {
674        let mut store = LedgerCommitStore::default();
675        store.commit(&ledger()).expect("initial ledger");
676        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
677        AllocationBootstrap::new(&mut store)
678            .validate_and_commit(snapshot, &TestPolicy, Some(42))
679            .expect("active commit");
680        let retirement = AllocationRetirement::new(
681            "app.users.v1",
682            MemoryManagerSlot::new(100).expect("usable slot"),
683        )
684        .expect("retirement");
685
686        let committed = AllocationBootstrap::new(&mut store)
687            .retire_and_commit(&retirement, Some(43))
688            .expect("retirement commit");
689
690        assert_eq!(committed.current_generation, 2);
691        assert_eq!(
692            committed.allocation_history.records()[0].state(),
693            AllocationState::Retired { generation: 2 }
694        );
695        assert_eq!(store.recover().unwrap().ledger(), &committed);
696    }
697
698    #[test]
699    fn retire_and_commit_rejects_unknown_key_before_commit() {
700        let mut store = LedgerCommitStore::default();
701        store.commit(&ledger()).expect("initial ledger");
702        let retirement = AllocationRetirement::new(
703            "app.users.v1",
704            MemoryManagerSlot::new(100).expect("usable slot"),
705        )
706        .expect("retirement");
707
708        let err = AllocationBootstrap::new(&mut store)
709            .retire_and_commit(&retirement, Some(43))
710            .expect_err("unknown key");
711        let recovered = store.recover().expect("recovered");
712
713        assert!(matches!(err, BootstrapRetirementError::Retirement(_)));
714        assert_eq!(recovered.current_generation(), 0);
715        assert_eq!(recovered.ledger().allocation_history().records(), []);
716    }
717}