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