liminal_protocol/lifecycle/checkpoint_codec/
frontier_accounting.rs1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum FrontierAccountingDecodeError {
13 Framing(CapsuleDecodeError),
15 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}