Skip to main content

liminal_protocol/lifecycle/checkpoint_codec/
frontier_accounting.rs

1//! Canonical scalar accounting and keyed retained-row charges.
2use super::super::{
3    CapsuleDecodeError, ClosureAccounting, ClosureAccountingError, ClosureState,
4    RetainedRecordCharge,
5};
6use super::{Reader, Writer, read_admission_order, read_list, write_admission_order, write_list};
7use crate::algebra::{ResourceVector, WideResourceVector};
8use alloc::vec::Vec;
9
10/// A framing refusal or the original scalar-accounting validation failure.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum FrontierAccountingDecodeError {
13    /// The bytes do not have the canonical field widths.
14    Framing(CapsuleDecodeError),
15    /// The decoded accounting violates its native invariant.
16    Accounting(ClosureAccountingError),
17}
18pub(in crate::lifecycle) fn encode_accounting(value: &ClosureAccounting) -> Vec<u8> {
19    let mut w = Writer::default();
20    w.put_u64(value.marker_capacity_credits());
21    w.put_u64(value.marker_anchors());
22    w.put_u64(value.edge_sequence_claims());
23    w.put_u64(value.edge_order_position_claims());
24    w.put_u64(value.edge_k_remaining().entries);
25    w.put_u64(value.edge_k_remaining().bytes);
26    w.put_u128(value.baseline().entries);
27    w.put_u128(value.baseline().bytes);
28    w.put_u64(value.configured_cap().entries);
29    w.put_u64(value.configured_cap().bytes);
30    w.put_u64(value.episode_churn_used());
31    w.put_u64(value.episode_churn_limit());
32    w.bytes
33}
34pub(in crate::lifecycle) fn decode_accounting(
35    state: ClosureState,
36    body: &[u8],
37) -> Result<ClosureAccounting, FrontierAccountingDecodeError> {
38    let read = || -> Result<_, CapsuleDecodeError> {
39        let mut r = Reader::new(body, 5);
40        let credits = r.take_u64()?;
41        let anchors = r.take_u64()?;
42        let sequence = r.take_u64()?;
43        let order = r.take_u64()?;
44        let remaining = ResourceVector {
45            entries: r.take_u64()?,
46            bytes: r.take_u64()?,
47        };
48        let baseline = WideResourceVector {
49            entries: r.take_u128()?,
50            bytes: r.take_u128()?,
51        };
52        let cap = ResourceVector {
53            entries: r.take_u64()?,
54            bytes: r.take_u64()?,
55        };
56        let churn = r.take_u64()?;
57        let limit = r.take_u64()?;
58        r.finish()?;
59        Ok((
60            credits, anchors, sequence, order, remaining, baseline, cap, churn, limit,
61        ))
62    };
63    let (credits, anchors, sequence, order, remaining, baseline, cap, churn, limit) =
64        read().map_err(FrontierAccountingDecodeError::Framing)?;
65    ClosureAccounting::try_new(
66        state, credits, anchors, sequence, order, remaining, baseline, cap, churn, limit,
67    )
68    .map_err(FrontierAccountingDecodeError::Accounting)
69}
70pub(in crate::lifecycle) fn encode_charges(values: &[RetainedRecordCharge]) -> Option<Vec<u8>> {
71    let mut w = Writer::default();
72    write_list(&mut w, values, |w, value| {
73        w.put_u64(value.delivery_seq());
74        write_admission_order(w, value.admission_order());
75        w.put_u64(value.encoded_charge().entries);
76        w.put_u64(value.encoded_charge().bytes);
77    });
78    w.finish()
79}
80pub(in crate::lifecycle) fn decode_charges(
81    body: &[u8],
82) -> Result<Vec<RetainedRecordCharge>, CapsuleDecodeError> {
83    let mut r = Reader::new(body, 6);
84    let charges = read_list(&mut r, |r| {
85        let sequence = r.take_u64()?;
86        let order = read_admission_order(r)?;
87        let charge = ResourceVector {
88            entries: r.take_u64()?,
89            bytes: r.take_u64()?,
90        };
91        if charge.entries != 1 {
92            return Err(r.invalid());
93        }
94        Ok(RetainedRecordCharge::new(sequence, order, charge))
95    })?;
96    r.finish()?;
97    for pair in charges.windows(2) {
98        if pair[0].delivery_seq().checked_add(1) != Some(pair[1].delivery_seq())
99            || pair[0].admission_order() >= pair[1].admission_order()
100        {
101            return Err(r.invalid());
102        }
103    }
104    Ok(charges)
105}
106
107#[cfg(test)]
108mod tests {
109    use super::super::super::AdmissionOrder;
110    use super::*;
111    use crate::outcome::CandidatePhase;
112
113    #[test]
114    fn checkpoint_accounting_canonical_bytes_and_refusals() -> Result<(), &'static str> {
115        let accounting = ClosureAccounting::try_new(
116            ClosureState::Clear,
117            8,
118            4,
119            0,
120            0,
121            ResourceVector::default(),
122            WideResourceVector {
123                entries: 3,
124                bytes: 97,
125            },
126            ResourceVector {
127                entries: 99,
128                bytes: 999,
129            },
130            7,
131            11,
132        );
133        let accounting = accounting.map_err(|_| "valid accounting refused")?;
134        let bytes = encode_accounting(&accounting);
135        assert_eq!(bytes.len(), 112);
136        let mut expected = Vec::new();
137        for value in [8_u64, 4, 0, 0, 0, 0] {
138            expected.extend_from_slice(&value.to_be_bytes());
139        }
140        expected.extend_from_slice(&3_u128.to_be_bytes());
141        expected.extend_from_slice(&97_u128.to_be_bytes());
142        for value in [99_u64, 999, 7, 11] {
143            expected.extend_from_slice(&value.to_be_bytes());
144        }
145        assert_eq!(bytes, expected);
146        assert_eq!(
147            decode_accounting(ClosureState::Clear, &bytes),
148            Ok(accounting)
149        );
150        let mut invalid = bytes;
151        invalid[15] = 9;
152        assert!(matches!(
153            decode_accounting(ClosureState::Clear, &invalid),
154            Err(FrontierAccountingDecodeError::Accounting(
155                ClosureAccountingError::MarkerAnchorsExceedCredits { .. }
156            ))
157        ));
158        invalid.push(0);
159        assert!(matches!(
160            decode_accounting(ClosureState::Clear, &invalid),
161            Err(FrontierAccountingDecodeError::Framing(_))
162        ));
163        Ok(())
164    }
165
166    #[test]
167    fn checkpoint_retained_charges_canonical_bytes_and_refusals() -> Result<(), &'static str> {
168        let order = AdmissionOrder::new(17, CandidatePhase::OrdinaryRecord, 3);
169        let charge = RetainedRecordCharge::new(
170            5,
171            order,
172            ResourceVector {
173                entries: 1,
174                bytes: 107,
175            },
176        );
177        let bytes = encode_charges(&[charge]).ok_or("bounded charge refused")?;
178        let mut expected = Writer::default();
179        expected.put_u32(1);
180        expected.put_u64(5);
181        write_admission_order(&mut expected, order);
182        expected.put_u64(1);
183        expected.put_u64(107);
184        assert_eq!(bytes, expected.bytes);
185        assert_eq!(decode_charges(&bytes), Ok(alloc::vec![charge]));
186        let mut invalid = bytes;
187        let entries_end = invalid.len() - 8;
188        invalid[entries_end - 1] = 0;
189        assert!(decode_charges(&invalid).is_err());
190        invalid.push(0);
191        assert!(decode_charges(&invalid).is_err());
192        Ok(())
193    }
194}