minerva 0.2.0

Causal ordering for distributed systems
extern crate alloc;

use alloc::collections::BTreeSet;
use alloc::vec::Vec;

use thiserror::Error;

use super::super::arrival::{Admission, Endorsement};
use super::super::gate::{EpochAddress, InvalidEpochAddress};
use super::shared::{WIRE_ADDRESS_LEN, encode_address_into, read_address_parts, read_u32};

/// Version tag for the canonical endorsement wire encoding (PRD 0028 R7:
/// the arrival round's first wire spelling, a new kind in its own version
/// namespace).
const ENDORSEMENT_WIRE_V1: u8 = 0x01;
/// Version, base address, and joiner count.
const ENDORSEMENT_WIRE_PREFIX_LEN: usize = 1 + WIRE_ADDRESS_LEN + 4;
/// The fixed prefix and 32-byte commitment. The count refusal proves that
/// a valid frame also carries at least one joiner.
const ENDORSEMENT_WIRE_FIXED_LEN: usize = ENDORSEMENT_WIRE_PREFIX_LEN + 32;

/// The default endorsement-joiner ceiling: the seal frame's admission
/// ceiling, because both sections spell the same boundary. Override with
/// [`EndorsementDecodeBudget::new`].
const ENDORSEMENT_DEFAULT_MAX_JOINERS: usize = 1024;

/// Ceiling for what an [`Endorsement`] decoder may materialize.
///
/// The frame's one variable-length section is the joiner set; the
/// unbudgeted door remains bounded by the input bytes.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct EndorsementDecodeBudget {
    max_joiners: usize,
}

impl EndorsementDecodeBudget {
    /// Allows at most `max_joiners` stations in the endorsed boundary.
    #[must_use]
    pub const fn new(max_joiners: usize) -> Self {
        Self { max_joiners }
    }

    /// The default ceiling: generous for any one boundary's growth, small
    /// enough to bound the decoder's amplification.
    #[must_use]
    pub const fn default_ceiling() -> Self {
        Self {
            max_joiners: ENDORSEMENT_DEFAULT_MAX_JOINERS,
        }
    }

    /// Maximum joiners in the endorsed boundary.
    #[must_use]
    pub const fn max_joiners(self) -> usize {
        self.max_joiners
    }
}

impl Default for EndorsementDecodeBudget {
    /// [`default_ceiling`](Self::default_ceiling).
    fn default() -> Self {
        Self::default_ceiling()
    }
}

/// Decode failure for an [`Endorsement`] wire frame.
#[non_exhaustive]
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
pub enum EndorsementDecodeError {
    /// The leading version byte is not recognized.
    #[error("unknown endorsement wire version: {0:#04x}")]
    UnknownVersion(u8),
    /// The input is too short for its count or carries trailing bytes.
    #[error("unexpected endorsement frame length: expected {expected}, found {found}")]
    UnexpectedLength {
        /// Required lower bound, or exact length for trailing input.
        expected: usize,
        /// Supplied input length.
        found: usize,
    },
    /// The base address is invalid.
    #[error("endorsement base: {0}")]
    Address(#[from] InvalidEpochAddress),
    /// An empty joiner set: an admission that admits nobody is not a
    /// boundary ([`Admission::new`] refuses the same set at the minting
    /// door).
    #[error("endorsement admission carries no joiners")]
    EmptyAdmission,
    /// The joiner set is not in canonical strictly ascending order.
    #[error("endorsement joiners not ascending: {found} after {previous}")]
    NonAscendingJoiners {
        /// Last well-ordered joiner.
        previous: u32,
        /// Out-of-order joiner.
        found: u32,
    },
    /// The joiner count exceeds the caller's ceiling.
    #[error("endorsement frame declares {count} joiners, past the budget's {budget}")]
    TooManyJoiners {
        /// Declared joiner count.
        count: u64,
        /// Caller budget.
        budget: u64,
    },
}

impl Endorsement {
    /// Encodes this endorsement as its canonical wire frame (PRD 0028 R7;
    /// the arrival round's first wire spelling): version, base address,
    /// joiner count, joiners ascending, and the 32-byte record commitment
    /// --- the admission section's own order, so the boundary spells
    /// identically wherever it rides.
    ///
    /// The speaker is not in the frame. The reporter is the authenticated
    /// envelope sender, exactly as for the confirmation and adoption
    /// reports: the decoded value is the claim half of a
    /// [`Vouched`](crate::metis::Vouched) report, and
    /// [`Vouched::trust`](crate::metis::Vouched::trust) binds it to the
    /// transport's identity at the caller's trust seam.
    #[must_use]
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut out = Vec::with_capacity(self.encoded_len());
        out.push(ENDORSEMENT_WIRE_V1);
        encode_address_into(&mut out, self.admission().base());
        let joiner_count = u32::try_from(self.admission().joiners().count()).unwrap_or(u32::MAX);
        out.extend_from_slice(&joiner_count.to_be_bytes());
        for joiner in self.admission().joiners() {
            out.extend_from_slice(&joiner.to_be_bytes());
        }
        out.extend_from_slice(self.commitment());
        out
    }

    /// The exact frame length, computed allocation-free.
    #[must_use]
    pub fn encoded_len(&self) -> usize {
        ENDORSEMENT_WIRE_PREFIX_LEN + self.admission().joiners().count() * 4 + 32
    }

    /// Decodes one canonical frame under the input-length ceiling,
    /// rejecting trailing bytes.
    ///
    /// The frame proves shape and only shape: a nonempty, strictly
    /// ascending joiner set over a valid base. Two laws deliberately live
    /// elsewhere. The base-to-round relation has no in-frame half --- an
    /// endorsement names no generation of its own, so
    /// [`Epochs::fold_endorsement`](crate::metis::Epochs::fold_endorsement)
    /// locates the base against the machine-held round. And station
    /// validity of the joiners is the consumer's authenticated-boundary
    /// law (ruling R-91): this codec accepts every `u32` station,
    /// including zero, exactly as every canonical Minerva codec does, and
    /// a consumer that narrows the station domain enforces that narrowing
    /// at its own door.
    ///
    /// # Errors
    ///
    /// Returns [`EndorsementDecodeError`] for an unknown version, invalid
    /// length, impossible base address, or a non-canonical joiner set.
    pub fn from_bytes(bytes: &[u8]) -> Result<Self, EndorsementDecodeError> {
        Self::decode(bytes, None)
    }

    /// Decodes one canonical frame under a caller-owned joiner ceiling.
    ///
    /// # Errors
    ///
    /// Returns [`EndorsementDecodeError`] for budget, structure, or
    /// content refusals.
    pub fn from_bytes_with_budget(
        bytes: &[u8],
        budget: EndorsementDecodeBudget,
    ) -> Result<Self, EndorsementDecodeError> {
        Self::decode(bytes, Some(budget))
    }

    fn decode(
        bytes: &[u8],
        budget: Option<EndorsementDecodeBudget>,
    ) -> Result<Self, EndorsementDecodeError> {
        if bytes.len() < ENDORSEMENT_WIRE_FIXED_LEN {
            return Err(EndorsementDecodeError::UnexpectedLength {
                expected: ENDORSEMENT_WIRE_FIXED_LEN,
                found: bytes.len(),
            });
        }
        let version = bytes[0];
        if version != ENDORSEMENT_WIRE_V1 {
            return Err(EndorsementDecodeError::UnknownVersion(version));
        }
        let short = EndorsementDecodeError::UnexpectedLength {
            expected: ENDORSEMENT_WIRE_FIXED_LEN,
            found: bytes.len(),
        };
        let ((generation, dot), rest) = read_address_parts(&bytes[1..]).ok_or(short)?;
        let base = EpochAddress::try_from_parts(generation, dot)?;
        let (count, mut rest) = read_u32(rest).ok_or(short)?;
        if count == 0 {
            return Err(EndorsementDecodeError::EmptyAdmission);
        }
        // O(1) impossible-count preflight: the joiner rows and the
        // commitment must both be backed by the remaining input.
        let backable = rest.len().saturating_sub(32) / 4;
        if count as usize > backable {
            let floor = u64::from(count).saturating_mul(4).saturating_add(32);
            return Err(EndorsementDecodeError::UnexpectedLength {
                expected: usize::try_from(
                    (ENDORSEMENT_WIRE_PREFIX_LEN as u64).saturating_add(floor),
                )
                .unwrap_or(usize::MAX),
                found: bytes.len(),
            });
        }
        if let Some(budget) = budget
            && count as usize > budget.max_joiners()
        {
            return Err(EndorsementDecodeError::TooManyJoiners {
                count: u64::from(count),
                budget: budget.max_joiners() as u64,
            });
        }
        let mut joiners = BTreeSet::new();
        let mut previous: Option<u32> = None;
        for _ in 0..count {
            let (joiner, tail) = read_u32(rest).ok_or(short)?;
            if let Some(previous) = previous
                && joiner <= previous
            {
                return Err(EndorsementDecodeError::NonAscendingJoiners {
                    previous,
                    found: joiner,
                });
            }
            previous = Some(joiner);
            let _ = joiners.insert(joiner);
            rest = tail;
        }
        let commitment: [u8; 32] = rest
            .get(..32)
            .and_then(|bytes| bytes.try_into().ok())
            .ok_or(short)?;
        let tail = &rest[32..];
        if !tail.is_empty() {
            return Err(EndorsementDecodeError::UnexpectedLength {
                expected: bytes.len() - tail.len(),
                found: bytes.len(),
            });
        }
        let admission = Admission::new(base, joiners)
            .map_err(|_refusal| EndorsementDecodeError::EmptyAdmission)?;
        Ok(Self::from_parts(admission, commitment))
    }
}