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