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