liminal-protocol 0.13.1

Shared participant-lifecycle protocol types for liminal
Documentation
//! Canonical scalar accounting and keyed retained-row charges.
use super::super::{
    CapsuleDecodeError, ClosureAccounting, ClosureAccountingError, ClosureState,
    RetainedRecordCharge,
};
use super::{Reader, Writer, read_admission_order, read_list, write_admission_order, write_list};
use crate::algebra::{ResourceVector, WideResourceVector};
use alloc::vec::Vec;

/// A framing refusal or the original scalar-accounting validation failure.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FrontierAccountingDecodeError {
    /// The bytes do not have the canonical field widths.
    Framing(CapsuleDecodeError),
    /// The decoded accounting violates its native invariant.
    Accounting(ClosureAccountingError),
}
pub(in crate::lifecycle) fn encode_accounting(value: &ClosureAccounting) -> Vec<u8> {
    let mut w = Writer::default();
    w.put_u64(value.marker_capacity_credits());
    w.put_u64(value.marker_anchors());
    w.put_u64(value.edge_sequence_claims());
    w.put_u64(value.edge_order_position_claims());
    w.put_u64(value.edge_k_remaining().entries);
    w.put_u64(value.edge_k_remaining().bytes);
    w.put_u128(value.baseline().entries);
    w.put_u128(value.baseline().bytes);
    w.put_u64(value.configured_cap().entries);
    w.put_u64(value.configured_cap().bytes);
    w.put_u64(value.episode_churn_used());
    w.put_u64(value.episode_churn_limit());
    w.bytes
}
pub(in crate::lifecycle) fn decode_accounting(
    state: ClosureState,
    body: &[u8],
) -> Result<ClosureAccounting, FrontierAccountingDecodeError> {
    let read = || -> Result<_, CapsuleDecodeError> {
        let mut r = Reader::new(body, 5);
        let credits = r.take_u64()?;
        let anchors = r.take_u64()?;
        let sequence = r.take_u64()?;
        let order = r.take_u64()?;
        let remaining = ResourceVector {
            entries: r.take_u64()?,
            bytes: r.take_u64()?,
        };
        let baseline = WideResourceVector {
            entries: r.take_u128()?,
            bytes: r.take_u128()?,
        };
        let cap = ResourceVector {
            entries: r.take_u64()?,
            bytes: r.take_u64()?,
        };
        let churn = r.take_u64()?;
        let limit = r.take_u64()?;
        r.finish()?;
        Ok((
            credits, anchors, sequence, order, remaining, baseline, cap, churn, limit,
        ))
    };
    let (credits, anchors, sequence, order, remaining, baseline, cap, churn, limit) =
        read().map_err(FrontierAccountingDecodeError::Framing)?;
    ClosureAccounting::try_new(
        state, credits, anchors, sequence, order, remaining, baseline, cap, churn, limit,
    )
    .map_err(FrontierAccountingDecodeError::Accounting)
}
pub(in crate::lifecycle) fn encode_charges(values: &[RetainedRecordCharge]) -> Option<Vec<u8>> {
    let mut w = Writer::default();
    write_list(&mut w, values, |w, value| {
        w.put_u64(value.delivery_seq());
        write_admission_order(w, value.admission_order());
        w.put_u64(value.encoded_charge().entries);
        w.put_u64(value.encoded_charge().bytes);
    });
    w.finish()
}
pub(in crate::lifecycle) fn decode_charges(
    body: &[u8],
) -> Result<Vec<RetainedRecordCharge>, CapsuleDecodeError> {
    let mut r = Reader::new(body, 6);
    let charges = read_list(&mut r, |r| {
        let sequence = r.take_u64()?;
        let order = read_admission_order(r)?;
        let charge = ResourceVector {
            entries: r.take_u64()?,
            bytes: r.take_u64()?,
        };
        if charge.entries != 1 {
            return Err(r.invalid());
        }
        Ok(RetainedRecordCharge::new(sequence, order, charge))
    })?;
    r.finish()?;
    for pair in charges.windows(2) {
        if pair[0].delivery_seq().checked_add(1) != Some(pair[1].delivery_seq())
            || pair[0].admission_order() >= pair[1].admission_order()
        {
            return Err(r.invalid());
        }
    }
    Ok(charges)
}

#[cfg(test)]
mod tests {
    use super::super::super::AdmissionOrder;
    use super::*;
    use crate::outcome::CandidatePhase;

    #[test]
    fn checkpoint_accounting_canonical_bytes_and_refusals() -> Result<(), &'static str> {
        let accounting = ClosureAccounting::try_new(
            ClosureState::Clear,
            8,
            4,
            0,
            0,
            ResourceVector::default(),
            WideResourceVector {
                entries: 3,
                bytes: 97,
            },
            ResourceVector {
                entries: 99,
                bytes: 999,
            },
            7,
            11,
        );
        let accounting = accounting.map_err(|_| "valid accounting refused")?;
        let bytes = encode_accounting(&accounting);
        assert_eq!(bytes.len(), 112);
        let mut expected = Vec::new();
        for value in [8_u64, 4, 0, 0, 0, 0] {
            expected.extend_from_slice(&value.to_be_bytes());
        }
        expected.extend_from_slice(&3_u128.to_be_bytes());
        expected.extend_from_slice(&97_u128.to_be_bytes());
        for value in [99_u64, 999, 7, 11] {
            expected.extend_from_slice(&value.to_be_bytes());
        }
        assert_eq!(bytes, expected);
        assert_eq!(
            decode_accounting(ClosureState::Clear, &bytes),
            Ok(accounting)
        );
        let mut invalid = bytes;
        invalid[15] = 9;
        assert!(matches!(
            decode_accounting(ClosureState::Clear, &invalid),
            Err(FrontierAccountingDecodeError::Accounting(
                ClosureAccountingError::MarkerAnchorsExceedCredits { .. }
            ))
        ));
        invalid.push(0);
        assert!(matches!(
            decode_accounting(ClosureState::Clear, &invalid),
            Err(FrontierAccountingDecodeError::Framing(_))
        ));
        Ok(())
    }

    #[test]
    fn checkpoint_retained_charges_canonical_bytes_and_refusals() -> Result<(), &'static str> {
        let order = AdmissionOrder::new(17, CandidatePhase::OrdinaryRecord, 3);
        let charge = RetainedRecordCharge::new(
            5,
            order,
            ResourceVector {
                entries: 1,
                bytes: 107,
            },
        );
        let bytes = encode_charges(&[charge]).ok_or("bounded charge refused")?;
        let mut expected = Writer::default();
        expected.put_u32(1);
        expected.put_u64(5);
        write_admission_order(&mut expected, order);
        expected.put_u64(1);
        expected.put_u64(107);
        assert_eq!(bytes, expected.bytes);
        assert_eq!(decode_charges(&bytes), Ok(alloc::vec![charge]));
        let mut invalid = bytes;
        let entries_end = invalid.len() - 8;
        invalid[entries_end - 1] = 0;
        assert!(decode_charges(&invalid).is_err());
        invalid.push(0);
        assert!(decode_charges(&invalid).is_err());
        Ok(())
    }
}