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