1use crate::{
2 declaration::DeclarationSnapshot,
3 key::StableKey,
4 ledger::{AllocationLedger, ClaimConflict, RecoveredLedger, validate_declaration_claim},
5 policy::RuntimeBootstrapPolicy,
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_key(&declaration.stable_key)
77 .map_err(AllocationValidationError::Policy)?;
78 policy
79 .validate_slot(&declaration.stable_key, &declaration.slot)
80 .map_err(AllocationValidationError::Policy)?;
81 }
82 validate_declaration_claim(ledger, declaration)
83 .map_err(|conflict| map_validation_claim_conflict(declaration, conflict))
84}
85
86fn map_validation_claim_conflict<P>(
87 declaration: &crate::declaration::AllocationDeclaration,
88 conflict: ClaimConflict<'_>,
89) -> AllocationValidationError<P> {
90 match conflict {
91 ClaimConflict::StableKeyMoved { record } => {
92 AllocationValidationError::StableKeySlotConflict {
93 stable_key: declaration.stable_key.clone(),
94 historical_slot: record.slot.clone(),
95 declared_slot: declaration.slot.clone(),
96 }
97 }
98 ClaimConflict::SlotReused { record } => AllocationValidationError::SlotStableKeyConflict {
99 slot: declaration.slot.clone(),
100 historical_key: record.stable_key.clone(),
101 declared_key: declaration.stable_key.clone(),
102 },
103 ClaimConflict::Tombstoned { record } => AllocationValidationError::RetiredAllocation {
104 stable_key: declaration.stable_key.clone(),
105 slot: record.slot.clone(),
106 },
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::{
114 declaration::AllocationDeclaration,
115 ledger::{AllocationRecord, AllocationState},
116 schema::SchemaMetadata,
117 slot::MemoryManagerSlot,
118 };
119
120 #[derive(Debug, Eq, PartialEq)]
121 struct TestPolicy;
122
123 impl RuntimeBootstrapPolicy for TestPolicy {
124 type Error = &'static str;
125
126 fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
127 if key.as_str().starts_with("bad.") {
128 return Err("bad key");
129 }
130 Ok(())
131 }
132
133 fn validate_slot(
134 &self,
135 key: &StableKey,
136 _slot: &MemoryManagerSlot,
137 ) -> Result<(), Self::Error> {
138 assert!(!key.as_str().starts_with("bad."));
139 Ok(())
140 }
141
142 fn runtime_bootstrap_identity(
143 &self,
144 ) -> Result<crate::PolicyIdentity, crate::PolicyIdentityError> {
145 crate::PolicyIdentity::new("validation-test", 1)
146 }
147 }
148
149 fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
150 AllocationLedger {
151 current_generation: 7,
152 records,
153 }
154 }
155
156 fn declaration(key: &str, id: u8) -> AllocationDeclaration {
157 AllocationDeclaration::new(
158 key,
159 MemoryManagerSlot::new(id).expect("usable slot"),
160 SchemaMetadata::default(),
161 )
162 .expect("declaration")
163 }
164
165 fn active_record(key: &str, id: u8) -> AllocationRecord {
166 AllocationRecord::active(&declaration(key, id))
167 }
168
169 fn recovered(records: Vec<AllocationRecord>) -> RecoveredLedger {
170 RecoveredLedger::from_trusted_ledger(ledger(records))
171 }
172
173 #[test]
174 fn accepts_matching_historical_owner() {
175 let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.users.v1", 100)])
176 .expect("snapshot");
177
178 check_allocations(
179 &recovered(vec![active_record("app.users.v1", 100)]),
180 &snapshot,
181 &TestPolicy,
182 )
183 .expect("validated");
184 }
185
186 #[test]
187 fn omitted_historical_records_do_not_fail_validation() {
188 let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.users.v1", 100)])
189 .expect("snapshot");
190
191 check_allocations(
192 &recovered(vec![
193 active_record("app.users.v1", 100),
194 active_record("app.orders.v1", 101),
195 ]),
196 &snapshot,
197 &TestPolicy,
198 )
199 .expect("omitted records are preserved, not retired");
200 }
201
202 #[test]
203 fn rejects_same_key_different_slot() {
204 let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.users.v1", 101)])
205 .expect("snapshot");
206
207 let err = check_allocations(
208 &recovered(vec![active_record("app.users.v1", 100)]),
209 &snapshot,
210 &TestPolicy,
211 )
212 .expect_err("conflict");
213
214 assert!(matches!(
215 err,
216 AllocationValidationError::StableKeySlotConflict { .. }
217 ));
218 }
219
220 #[test]
221 fn rejects_same_slot_different_key() {
222 let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.orders.v1", 100)])
223 .expect("snapshot");
224
225 let err = check_allocations(
226 &recovered(vec![active_record("app.users.v1", 100)]),
227 &snapshot,
228 &TestPolicy,
229 )
230 .expect_err("conflict");
231
232 assert!(matches!(
233 err,
234 AllocationValidationError::SlotStableKeyConflict { .. }
235 ));
236 }
237
238 #[test]
239 fn rejects_retired_redeclaration() {
240 let mut record = active_record("app.users.v1", 100);
241 record.state = AllocationState::Retired;
242 let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("app.users.v1", 100)])
243 .expect("snapshot");
244
245 let err = check_allocations(&recovered(vec![record]), &snapshot, &TestPolicy)
246 .expect_err("retired");
247
248 assert!(matches!(
249 err,
250 AllocationValidationError::RetiredAllocation { .. }
251 ));
252 }
253
254 #[test]
255 fn policy_rejections_fail_before_validation_succeeds() {
256 let snapshot = DeclarationSnapshot::canonicalize(vec![declaration("bad.users.v1", 100)])
257 .expect("snapshot");
258
259 let err = check_allocations(&recovered(Vec::new()), &snapshot, &TestPolicy)
260 .expect_err("policy failure");
261
262 assert_eq!(err, AllocationValidationError::Policy("bad key"));
263 }
264
265 #[test]
266 fn full_slot_domain_validates_stages_and_commits() {
267 let mut store = crate::LedgerCommitStore::default();
268 let recovered = store.recover_or_genesis().unwrap();
269 let snapshot = DeclarationSnapshot::canonicalize(
270 (0..=crate::MEMORY_MANAGER_MAX_ID)
271 .map(|id| declaration(&format!("app.store{id}.v1"), id))
272 .collect(),
273 )
274 .unwrap();
275 let (staged, _) =
276 crate::ledger::stage_generation(recovered, snapshot, &TestPolicy).unwrap();
277 assert_eq!(staged.records().len(), 255);
278
279 let committed = store.commit(&staged).unwrap();
280 assert_eq!(committed.current_generation(), 1);
281 assert_eq!(store.recover().unwrap(), committed);
282 }
283}