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