1use crate::{
2 key::StableKey,
3 ledger::{AllocationLedger, ClaimConflict, RecoveredLedger, validate_declaration_claim},
4 policy::RuntimeBootstrapPolicy,
5 requests::DeclarationSnapshot,
6 slot::MemoryManagerSlot,
7};
8
9#[non_exhaustive]
18#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
19pub enum AllocationValidationError<P> {
20 #[error("allocation policy rejected a declaration")]
22 Policy(P),
23 #[error("stable key '{stable_key}' was historically bound to a different allocation slot")]
25 StableKeySlotConflict {
26 stable_key: StableKey,
28 historical_slot: MemoryManagerSlot,
30 declared_slot: MemoryManagerSlot,
32 },
33 #[error("allocation slot '{slot:?}' was historically bound to stable key '{historical_key}'")]
35 SlotStableKeyConflict {
36 slot: MemoryManagerSlot,
38 historical_key: StableKey,
40 declared_key: StableKey,
42 },
43 #[error("stable key '{stable_key}' was explicitly retired and cannot be redeclared")]
45 RetiredAllocation {
46 stable_key: StableKey,
48 slot: MemoryManagerSlot,
50 },
51}
52
53pub fn check_allocations<P: RuntimeBootstrapPolicy>(
56 recovered: &RecoveredLedger,
57 snapshot: &DeclarationSnapshot,
58 policy: &P,
59) -> Result<(), AllocationValidationError<P::Error>> {
60 for declaration in snapshot.declarations() {
61 check_declaration(recovered.ledger(), declaration, policy)?;
62 }
63
64 Ok(())
65}
66
67pub fn check_declaration<P: RuntimeBootstrapPolicy>(
68 ledger: &AllocationLedger,
69 declaration: &crate::declaration::AllocationDeclaration,
70 policy: &P,
71) -> Result<crate::ledger::ClaimOutcome, AllocationValidationError<P::Error>> {
72 if declaration.stable_key.as_str() != crate::IC_MEMORY_LEDGER_STABLE_KEY {
75 policy
76 .validate_allocation(&declaration.stable_key, &declaration.slot)
77 .map_err(AllocationValidationError::Policy)?;
78 }
79 validate_declaration_claim(ledger, declaration)
80 .map_err(|conflict| map_validation_claim_conflict(declaration, conflict))
81}
82
83fn map_validation_claim_conflict<P>(
84 declaration: &crate::declaration::AllocationDeclaration,
85 conflict: ClaimConflict<'_>,
86) -> AllocationValidationError<P> {
87 match conflict {
88 ClaimConflict::StableKeyMoved { record } => {
89 AllocationValidationError::StableKeySlotConflict {
90 stable_key: declaration.stable_key.clone(),
91 historical_slot: record.slot.clone(),
92 declared_slot: declaration.slot.clone(),
93 }
94 }
95 ClaimConflict::SlotReused { record } => AllocationValidationError::SlotStableKeyConflict {
96 slot: declaration.slot.clone(),
97 historical_key: record.stable_key.clone(),
98 declared_key: declaration.stable_key.clone(),
99 },
100 ClaimConflict::Tombstoned { record } => AllocationValidationError::RetiredAllocation {
101 stable_key: declaration.stable_key.clone(),
102 slot: record.slot.clone(),
103 },
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::{
111 declaration::AllocationDeclaration,
112 ledger::{AllocationRecord, AllocationState},
113 slot::MemoryManagerSlot,
114 };
115
116 #[derive(Debug, Eq, PartialEq)]
117 struct TestPolicy;
118
119 impl RuntimeBootstrapPolicy for TestPolicy {
120 type Error = &'static str;
121
122 fn validate_allocation(
123 &self,
124 key: &StableKey,
125 _slot: &MemoryManagerSlot,
126 ) -> Result<(), Self::Error> {
127 if key.as_str().starts_with("bad.") {
128 return Err("bad key");
129 }
130 assert!(!key.as_str().starts_with("bad."));
131 Ok(())
132 }
133
134 fn runtime_bootstrap_identity(
135 &self,
136 ) -> Result<crate::PolicyIdentity, crate::PolicyIdentityError> {
137 crate::PolicyIdentity::new("validation-test", 1)
138 }
139 }
140
141 fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
142 AllocationLedger {
143 current_generation: 7,
144 records,
145 }
146 }
147
148 fn declaration(key: &str, id: u8) -> AllocationDeclaration {
149 AllocationDeclaration::new(key, MemoryManagerSlot::new(id).expect("usable slot"))
150 .expect("declaration")
151 }
152
153 fn active_record(key: &str, id: u8) -> AllocationRecord {
154 AllocationRecord::active(&declaration(key, id))
155 }
156
157 fn recovered(records: Vec<AllocationRecord>) -> RecoveredLedger {
158 RecoveredLedger::from_trusted_ledger(ledger(records))
159 }
160
161 #[test]
162 fn accepts_matching_historical_owner() {
163 let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 100)]);
164
165 check_allocations(
166 &recovered(vec![active_record("app.users.v1", 100)]),
167 &snapshot,
168 &TestPolicy,
169 )
170 .expect("validated");
171 }
172
173 #[test]
174 fn omitted_historical_records_do_not_fail_validation() {
175 let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 100)]);
176
177 check_allocations(
178 &recovered(vec![
179 active_record("app.users.v1", 100),
180 active_record("app.orders.v1", 101),
181 ]),
182 &snapshot,
183 &TestPolicy,
184 )
185 .expect("omitted records are preserved, not retired");
186 }
187
188 #[test]
189 fn rejects_same_key_different_slot() {
190 let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 101)]);
191
192 let err = check_allocations(
193 &recovered(vec![active_record("app.users.v1", 100)]),
194 &snapshot,
195 &TestPolicy,
196 )
197 .expect_err("conflict");
198
199 assert!(matches!(
200 err,
201 AllocationValidationError::StableKeySlotConflict { .. }
202 ));
203 }
204
205 #[test]
206 fn rejects_same_slot_different_key() {
207 let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.orders.v1", 100)]);
208
209 let err = check_allocations(
210 &recovered(vec![active_record("app.users.v1", 100)]),
211 &snapshot,
212 &TestPolicy,
213 )
214 .expect_err("conflict");
215
216 assert!(matches!(
217 err,
218 AllocationValidationError::SlotStableKeyConflict { .. }
219 ));
220 }
221
222 #[test]
223 fn rejects_retired_redeclaration() {
224 let mut record = active_record("app.users.v1", 100);
225 record.state = AllocationState::Retired;
226 let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 100)]);
227
228 let err = check_allocations(&recovered(vec![record]), &snapshot, &TestPolicy)
229 .expect_err("retired");
230
231 assert!(matches!(
232 err,
233 AllocationValidationError::RetiredAllocation { .. }
234 ));
235 }
236
237 #[test]
238 fn policy_rejections_fail_before_validation_succeeds() {
239 let snapshot = DeclarationSnapshot::for_test(vec![declaration("bad.users.v1", 100)]);
240
241 let err = check_allocations(&recovered(Vec::new()), &snapshot, &TestPolicy)
242 .expect_err("policy failure");
243
244 assert_eq!(err, AllocationValidationError::Policy("bad key"));
245 }
246
247 #[test]
248 fn full_slot_domain_validates_stages_and_commits() {
249 let mut store = crate::LedgerCommitStore::default();
250 let recovered = store.recover_or_genesis().unwrap();
251 let snapshot = DeclarationSnapshot::for_test(
252 (0..=crate::MEMORY_MANAGER_MAX_ID)
253 .map(|id| declaration(&format!("app.store{id}.v1"), id))
254 .collect(),
255 );
256 let (staged, _) =
257 crate::ledger::stage_generation(recovered, snapshot, &TestPolicy).unwrap();
258 assert_eq!(staged.records().len(), 255);
259
260 let committed = store.commit(&staged).unwrap();
261 assert_eq!(committed.current_generation(), 1);
262 assert_eq!(store.recover().unwrap(), committed);
263 }
264}