Skip to main content

ic_memory/
bootstrap.rs

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