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(&self) -> crate::PolicyIdentity {
135 crate::PolicyIdentity::new(*b"validation-test.................")
136 }
137 }
138
139 fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
140 AllocationLedger {
141 current_generation: 7,
142 records,
143 }
144 }
145
146 fn declaration(key: &str, id: u8) -> AllocationDeclaration {
147 AllocationDeclaration::new(key, MemoryManagerSlot::new(id).expect("usable slot"))
148 .expect("declaration")
149 }
150
151 fn active_record(key: &str, id: u8) -> AllocationRecord {
152 AllocationRecord::active(&declaration(key, id))
153 }
154
155 fn recovered(records: Vec<AllocationRecord>) -> RecoveredLedger {
156 RecoveredLedger::from_trusted_ledger(ledger(records))
157 }
158
159 #[test]
160 fn accepts_matching_historical_owner() {
161 let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 100)]);
162
163 check_allocations(
164 &recovered(vec![active_record("app.users.v1", 100)]),
165 &snapshot,
166 &TestPolicy,
167 )
168 .expect("validated");
169 }
170
171 #[test]
172 fn omitted_historical_records_do_not_fail_validation() {
173 let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 100)]);
174
175 check_allocations(
176 &recovered(vec![
177 active_record("app.users.v1", 100),
178 active_record("app.orders.v1", 101),
179 ]),
180 &snapshot,
181 &TestPolicy,
182 )
183 .expect("omitted records are preserved, not retired");
184 }
185
186 #[test]
187 fn rejects_same_key_different_slot() {
188 let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 101)]);
189
190 let err = check_allocations(
191 &recovered(vec![active_record("app.users.v1", 100)]),
192 &snapshot,
193 &TestPolicy,
194 )
195 .expect_err("conflict");
196
197 assert!(matches!(
198 err,
199 AllocationValidationError::StableKeySlotConflict { .. }
200 ));
201 }
202
203 #[test]
204 fn rejects_same_slot_different_key() {
205 let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.orders.v1", 100)]);
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::SlotStableKeyConflict { .. }
217 ));
218 }
219
220 #[test]
221 fn rejects_retired_redeclaration() {
222 let mut record = active_record("app.users.v1", 100);
223 record.state = AllocationState::Retired;
224 let snapshot = DeclarationSnapshot::for_test(vec![declaration("app.users.v1", 100)]);
225
226 let err = check_allocations(&recovered(vec![record]), &snapshot, &TestPolicy)
227 .expect_err("retired");
228
229 assert!(matches!(
230 err,
231 AllocationValidationError::RetiredAllocation { .. }
232 ));
233 }
234
235 #[test]
236 fn policy_rejections_fail_before_validation_succeeds() {
237 let snapshot = DeclarationSnapshot::for_test(vec![declaration("bad.users.v1", 100)]);
238
239 let err = check_allocations(&recovered(Vec::new()), &snapshot, &TestPolicy)
240 .expect_err("policy failure");
241
242 assert_eq!(err, AllocationValidationError::Policy("bad key"));
243 }
244
245 #[test]
246 fn full_slot_domain_validates_stages_and_commits() {
247 let mut store = crate::LedgerCommitStore::default();
248 let recovered = store.recover_or_genesis().unwrap();
249 let snapshot = DeclarationSnapshot::for_test(
250 (0..=crate::MEMORY_MANAGER_MAX_ID)
251 .map(|id| declaration(&format!("app.store{id}.v1"), id))
252 .collect(),
253 );
254 let (staged, _) =
255 crate::ledger::stage_generation(recovered, snapshot, &TestPolicy).unwrap();
256 assert_eq!(staged.records().len(), 255);
257
258 let committed = store.commit(&staged).unwrap();
259 assert_eq!(committed.current_generation(), 1);
260 assert_eq!(store.recover().unwrap(), committed);
261 }
262}