pic_continuity/authority/
attenuation.rs1use super::bitmap::RemoveBitmap;
34use super::indexed::{IndexedAuthorityMap, KvTuple};
35use crate::error::RejectReason;
36use std::collections::BTreeMap;
37
38#[derive(Debug, Clone, Default)]
40pub struct Attenuations {
41 pub identity_context: Option<RemoveBitmap>,
43 pub invariants: Option<RemoveBitmap>,
45 pub execution_contract_additions: Vec<KvTuple>,
48}
49
50impl Attenuations {
51 pub fn is_empty(&self) -> bool {
53 self.identity_context.is_none()
54 && self.invariants.is_none()
55 && self.execution_contract_additions.is_empty()
56 }
57}
58
59fn apply_removal<T: Clone>(
62 section: &BTreeMap<u32, T>,
63 bitmap: Option<&RemoveBitmap>,
64 section_name: &'static str,
65) -> Result<BTreeMap<u32, T>, RejectReason> {
66 match bitmap {
67 None => Ok(section.clone()),
68 Some(bm) => {
69 bm.validate_against(section.len() as u32, section_name)?;
70 let removed = bm.indices();
71 let survivors = section
72 .iter()
73 .filter(|(i, _)| !removed.contains(i))
74 .map(|(_, t)| t.clone());
75 Ok(survivors.enumerate().map(|(i, t)| (i as u32, t)).collect())
76 }
77 }
78}
79
80fn apply_additions(
83 section: &BTreeMap<u32, KvTuple>,
84 additions: &[KvTuple],
85) -> Result<BTreeMap<u32, KvTuple>, RejectReason> {
86 let mut out = section.clone();
87 if additions.is_empty() {
88 return Ok(out);
89 }
90
91 let mut accepted: Vec<KvTuple> = Vec::with_capacity(additions.len());
92 for (key, value) in additions {
93 value.validate(key)?;
94 accepted.push((key.clone(), value.clone()));
95 }
96 accepted.sort_by(|a, b| a.0.cmp(&b.0));
98 for pair in accepted.windows(2) {
100 if pair[0].0 == pair[1].0 {
101 return Err(RejectReason::DuplicateAdditionKey(pair[0].0.clone()));
102 }
103 }
104
105 let base = out.len() as u32;
106 for (offset, tuple) in accepted.into_iter().enumerate() {
107 out.insert(base + offset as u32, tuple);
108 }
109 Ok(out)
110}
111
112pub fn materialize(
116 predecessor: &IndexedAuthorityMap,
117 attenuations: &Attenuations,
118) -> Result<IndexedAuthorityMap, RejectReason> {
119 let identity_context = match &predecessor.identity_context {
120 None => {
121 if attenuations.identity_context.is_some() {
122 return Err(RejectReason::BitmapIndexOutOfRange("identity_context"));
123 }
124 None
125 }
126 Some(section) => {
127 let applied = apply_removal(
128 section,
129 attenuations.identity_context.as_ref(),
130 "identity_context",
131 )?;
132 if applied.is_empty() {
133 None
134 } else {
135 Some(applied)
136 }
137 }
138 };
139
140 let invariants = apply_removal(
141 &predecessor.invariants,
142 attenuations.invariants.as_ref(),
143 "invariants",
144 )?;
145
146 let execution_contract = apply_additions(
147 &predecessor.execution_contract,
148 &attenuations.execution_contract_additions,
149 )?;
150
151 Ok(IndexedAuthorityMap {
152 identity_context,
153 invariants,
154 execution_contract,
155 })
156}
157
158pub trait AttenuationOrder {
162 fn attenuates(&self, current: &IndexedAuthorityMap, predecessor: &IndexedAuthorityMap) -> bool;
165}
166
167#[derive(Debug, Clone, Copy, Default)]
171pub struct ReferenceProfile;
172
173impl AttenuationOrder for ReferenceProfile {
174 fn attenuates(&self, current: &IndexedAuthorityMap, predecessor: &IndexedAuthorityMap) -> bool {
175 let invariants_ok = current
177 .invariants
178 .values()
179 .all(|t| predecessor.contains_invariant(t));
180
181 let identity_ok = match (¤t.identity_context, &predecessor.identity_context) {
183 (None, _) => true,
184 (Some(_), None) => false,
185 (Some(cur), Some(pred)) => cur.values().all(|t| pred.values().any(|p| p == t)),
186 };
187
188 let contract_ok = predecessor
190 .execution_contract
191 .values()
192 .all(|t| current.contains_contract_entry(t));
193
194 invariants_ok && identity_ok && contract_ok
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::authority::indexed::{InvariantTuple, TupleValue};
202 use crate::authority::{AuthorityValue, Invariant, LogicalAuthority};
203
204 fn walkthrough_pca0() -> IndexedAuthorityMap {
205 let mut contract = std::collections::BTreeMap::new();
206 contract.insert("corporation".into(), AuthorityValue::One("ACME".into()));
207 contract.insert(
208 "department".into(),
209 AuthorityValue::One("sensitive-documents".into()),
210 );
211 let logical = LogicalAuthority::new(
212 None,
213 vec![
214 Invariant::new(
215 "documents:read:document-42",
216 "read",
217 "documents",
218 "document-42",
219 ),
220 Invariant::new("storage:save", "save", "storage", "*"),
221 ],
222 contract,
223 );
224 IndexedAuthorityMap::from_logical(&logical).unwrap()
225 }
226
227 #[test]
230 fn walkthrough_two_hops_reindex() {
231 let pca0 = walkthrough_pca0();
232 assert_eq!(pca0.invariants[&0].0, "documents:read:document-42");
233 assert_eq!(pca0.invariants[&1].0, "storage:save");
234
235 let att1 = Attenuations {
237 invariants: RemoveBitmap::from_indices(&[0]),
238 ..Default::default()
239 };
240 assert_eq!(att1.invariants.as_ref().unwrap().bytes(), &[0x01]);
241 let pca1 = materialize(&pca0, &att1).unwrap();
242 assert_eq!(pca1.invariants.len(), 1);
243 assert_eq!(pca1.invariants[&0].0, "storage:save"); let att2 = Attenuations {
248 invariants: RemoveBitmap::from_indices(&[0]),
249 ..Default::default()
250 };
251 let pca2 = materialize(&pca1, &att2).unwrap();
252 assert!(pca2.invariants.is_empty());
253 assert_eq!(pca2.execution_contract.len(), 2);
255 }
256
257 #[test]
258 fn dropped_authority_cannot_reappear() {
259 let pca0 = walkthrough_pca0();
260 let att = Attenuations {
261 invariants: RemoveBitmap::from_indices(&[0]),
262 ..Default::default()
263 };
264 let pca1 = materialize(&pca0, &att).unwrap();
265
266 let dropped = InvariantTuple(
267 "documents:read:document-42".into(),
268 "read".into(),
269 "documents".into(),
270 "document-42".into(),
271 );
272 assert!(!pca1.contains_invariant(&dropped));
273 }
276
277 #[test]
278 fn contract_additions_sorted_appended_and_deduplicated() {
279 let pca0 = walkthrough_pca0();
280 let att = Attenuations {
281 execution_contract_additions: vec![
282 ("region".into(), TupleValue::Text("EU".into())),
283 ("audit".into(), TupleValue::Text("required".into())),
284 ],
285 ..Default::default()
286 };
287 let next = materialize(&pca0, &att).unwrap();
288 assert_eq!(next.execution_contract[&0].0, "corporation");
291 assert_eq!(next.execution_contract[&1].0, "department");
292 assert_eq!(next.execution_contract[&2].0, "audit");
293 assert_eq!(next.execution_contract[&3].0, "region");
294
295 let dup = Attenuations {
296 execution_contract_additions: vec![
297 ("region".into(), TupleValue::Text("EU".into())),
298 ("region".into(), TupleValue::Text("US".into())),
299 ],
300 ..Default::default()
301 };
302 assert_eq!(
303 materialize(&pca0, &dup).unwrap_err(),
304 RejectReason::DuplicateAdditionKey("region".into())
305 );
306 }
307
308 #[test]
309 fn bitmap_out_of_range_rejected() {
310 let pca0 = walkthrough_pca0();
311 let att = Attenuations {
312 invariants: RemoveBitmap::from_indices(&[2]),
313 ..Default::default()
314 };
315 assert_eq!(
316 materialize(&pca0, &att).unwrap_err(),
317 RejectReason::BitmapIndexOutOfRange("invariants")
318 );
319 }
320
321 #[test]
322 fn reference_profile_non_expansion() {
323 let pca0 = walkthrough_pca0();
324 let att = Attenuations {
325 invariants: RemoveBitmap::from_indices(&[0]),
326 ..Default::default()
327 };
328 let pca1 = materialize(&pca0, &att).unwrap();
329
330 let order = ReferenceProfile;
331 assert!(order.attenuates(&pca1, &pca0));
332 assert!(!order.attenuates(&pca0, &pca1));
334 }
335}