Skip to main content

ic_memory/
bootstrap.rs

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