Skip to main content

pic_continuity/authority/
attenuation.rs

1/*
2 * Copyright Nitro Agility S.r.l.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! Attenuation and materialization (Profile 0.2).
18//!
19//! - Removal attenuation applies only to `identity_context` and
20//!   `invariants`, by section-local removal bitmap. A removed entry cannot
21//!   reappear later in the same continuity.
22//! - Execution-contract restriction is addition-only: proposed `[key, value]`
23//!   tuples are validated, deduplicated by canonical key, sorted by canonical
24//!   key (Unicode code point order), and appended with the **next**
25//!   section-local indexes assigned by the settlement verifier — never by the
26//!   workload. Constraints accumulate with logical AND.
27//!
28//! [`materialize`] produces the successor Indexed Authority Map; sections
29//! subject to removal are re-materialized with fresh contiguous indexes, so
30//! the same wire bitmap can address different entries at different
31//! checkpoints (as in the reference walkthrough).
32
33use super::bitmap::RemoveBitmap;
34use super::indexed::{IndexedAuthorityMap, KvTuple};
35use crate::error::RejectReason;
36use std::collections::BTreeMap;
37
38/// Parsed attenuations of one transition.
39#[derive(Debug, Clone, Default)]
40pub struct Attenuations {
41    /// Removal bitmap over the `identity_context` section, if any.
42    pub identity_context: Option<RemoveBitmap>,
43    /// Removal bitmap over the `invariants` section, if any.
44    pub invariants: Option<RemoveBitmap>,
45    /// Proposed execution-contract additions, as canonical `[key, value]`
46    /// tuples (unindexed: the verifier assigns indexes).
47    pub execution_contract_additions: Vec<KvTuple>,
48}
49
50impl Attenuations {
51    /// `true` when the transition proposes no authority change.
52    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
59/// Applies a removal bitmap to an indexed section, re-materializing the
60/// survivors with fresh contiguous indexes in their canonical relative order.
61fn 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
80/// Validates, deduplicates, and sorts proposed execution-contract additions,
81/// then appends them to the section with the next section-local indexes.
82fn 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    // Sort by canonical key; input array order never determines indexes.
97    accepted.sort_by(|a, b| a.0.cmp(&b.0));
98    // More than one accepted addition with the same canonical key is invalid.
99    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
112/// Materializes the successor authority: predecessor map plus the accepted
113/// attenuations of one transition. Removal never adds authority; contract
114/// entries are never removed, replaced, or weakened.
115pub 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
158/// The profile-defined attenuation order `≤` used for non-expansion
159/// validation. A Verifier rejects a state whose order it cannot evaluate
160/// deterministically.
161pub trait AttenuationOrder {
162    /// Returns true when `current` is equal to or more restrictive than
163    /// `predecessor`.
164    fn attenuates(&self, current: &IndexedAuthorityMap, predecessor: &IndexedAuthorityMap) -> bool;
165}
166
167/// The reference attenuation profile: invariants and identity entries use
168/// subset inclusion; execution-contract constraints may only be preserved or
169/// extended (accumulating with AND).
170#[derive(Debug, Clone, Copy, Default)]
171pub struct ReferenceProfile;
172
173impl AttenuationOrder for ReferenceProfile {
174    fn attenuates(&self, current: &IndexedAuthorityMap, predecessor: &IndexedAuthorityMap) -> bool {
175        // invariants(current) ⊆ invariants(predecessor)
176        let invariants_ok = current
177            .invariants
178            .values()
179            .all(|t| predecessor.contains_invariant(t));
180
181        // identity entries(current) ⊆ identity entries(predecessor)
182        let identity_ok = match (&current.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        // every predecessor contract entry is preserved in current
189        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    /// The centralized-exchange walkthrough: two hops, both `h'01'`, because
228    /// every checkpoint re-materializes its section-local indexes.
229    #[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        // Worker 1: remove index 0 (the read invariant).
236        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"); // re-indexed to 0
244
245        // Worker 2: the bitmap is h'01' again because storage:save is now
246        // section-local index 0.
247        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        // The execution contract remains.
254        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        // Removal is the only invariant mechanism: there is no encoding that
274        // adds an invariant, so reappearance is unrepresentable by grammar.
275    }
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        // Existing entries keep their indexes; additions are appended in
289        // canonical key order with the next indexes.
290        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        // The reverse direction expands authority.
333        assert!(!order.attenuates(&pca0, &pca1));
334    }
335}