minerva 0.2.0

Causal ordering for distributed systems
extern crate alloc;

use alloc::vec::Vec;

use crate::kairos::{KAIROS_WIRE_LEN, Kairos};
use crate::metis::Cut;
use crate::metis::dot_set::HAVE_SET_WIRE_HEADER_LEN;
use thiserror::Error;

use super::super::Declaration;
use super::super::arrival::Admission;
use super::super::gate::{EpochAddress, InvalidEpochAddress};
use super::error::WireCutError;
use super::shared::{
    WIRE_ADDRESS_LEN, WIRE_DOT_LEN, cut_embedding_len, decode_cut, encode_cut_into,
    encode_dot_into, read_address_parts, read_u32,
};
use alloc::collections::BTreeSet;

/// Version tag for the canonical [`Declaration`] wire encoding.
const DECLARATION_WIRE_V1: u8 = 0x01;
/// Version tag for the admission-bearing sibling encoding (PRD 0028 R7,
/// ruling R-89): the version-1 layout followed by one admission section.
/// Emitted exactly when the declaration carries a boundary, so each
/// version keeps its own canonical bijection and a version-1 reader
/// refuses the tagged frame whole rather than shedding the boundary.
const DECLARATION_WIRE_V2: u8 = 0x02;
/// Version, generation, declaration dot, and rank.
const DECLARATION_WIRE_PREFIX_LEN: usize = 1 + 8 + WIRE_DOT_LEN + KAIROS_WIRE_LEN;
/// The fixed prefix plus an empty cut embedding.
const DECLARATION_WIRE_MIN_LEN: usize = DECLARATION_WIRE_PREFIX_LEN + HAVE_SET_WIRE_HEADER_LEN;

/// Decode failure for a [`Declaration`] wire frame.
#[non_exhaustive]
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeclarationDecodeError {
    /// The leading version byte is not recognized.
    #[error("unknown declaration wire version: {0:#04x}")]
    UnknownVersion(u8),
    /// The input is too short or carries trailing bytes.
    #[error("unexpected declaration frame length: expected {expected}, found {found}")]
    UnexpectedLength {
        /// Required lower bound, or exact length for trailing input.
        expected: usize,
        /// Supplied input length.
        found: usize,
    },
    /// The generation or declaration dot is not a valid address.
    #[error("declaration address: {0}")]
    Address(#[from] InvalidEpochAddress),
    /// The rank stamp is malformed.
    #[error("declaration rank: {0}")]
    Rank(crate::kairos::DecodeError),
    /// The witnessed-cut embedding refused.
    #[error("declaration cut: {0}")]
    Cut(#[from] WireCutError),
    /// A version-2 frame with an empty joiner set: an admission that
    /// admits nobody is not a boundary, and the version-1 spelling is the
    /// canonical one for a plain declaration.
    #[error("declaration admission carries no joiners")]
    EmptyAdmission,
    /// The admission's joiner set is not in canonical ascending order.
    #[error("declaration joiners not ascending: {found} after {previous}")]
    NonAscendingJoiners {
        /// Last well-ordered joiner.
        previous: u32,
        /// Out-of-order joiner.
        found: u32,
    },
    /// The admission's base is not the declared generation's predecessor:
    /// a boundary rides exactly the window over its base (PRD 0028 R2),
    /// so any other base is a claim no declarer can make.
    #[error("declaration admission base at generation {base}, declaration at {declaration}")]
    AdmissionBaseMismatch {
        /// The admission base's generation.
        base: u64,
        /// The declared generation.
        declaration: u64,
    },
}

impl Declaration {
    /// Encodes this declaration as its canonical wire frame (S284; the PRD
    /// 0025 kind-5 payload). Version 1 spells version, generation, declaration
    /// dot, rank, and witnessed cut. When the declaration carries an
    /// admission, and only then, the encoder emits the version-2 sibling
    /// (S342, PRD 0028 R7). Version 2 appends the admission section: base
    /// address, joiner count, joiners ascending.
    ///
    /// Every accepted frame re-encodes to its own bytes, per version. The
    /// sibling discipline holds: version 2 is a distinct byte under the
    /// same kind, never an in-place evolution, and a version-1 reader
    /// refuses a tagged frame whole rather than shedding the boundary.
    #[must_use]
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut out = Vec::with_capacity(self.encoded_len());
        out.push(if self.admission.is_some() {
            DECLARATION_WIRE_V2
        } else {
            DECLARATION_WIRE_V1
        });
        out.extend_from_slice(&self.address.generation().to_be_bytes());
        encode_dot_into(&mut out, self.address.declaration());
        out.extend_from_slice(&self.rank.to_bytes());
        encode_cut_into(&mut out, self.cut.as_vector());
        if let Some(admission) = &self.admission {
            out.extend_from_slice(&admission.base().generation().to_be_bytes());
            encode_dot_into(&mut out, admission.base().declaration());
            let joiner_count = u32::try_from(admission.joiners().count()).unwrap_or(u32::MAX);
            out.extend_from_slice(&joiner_count.to_be_bytes());
            for joiner in admission.joiners() {
                out.extend_from_slice(&joiner.to_be_bytes());
            }
            if let Some(commitment) = &self.admission_commitment {
                out.extend_from_slice(commitment);
            }
        }
        out
    }

    /// The exact frame length, computed allocation-free.
    #[must_use]
    pub fn encoded_len(&self) -> usize {
        DECLARATION_WIRE_PREFIX_LEN
            + cut_embedding_len(self.cut.as_vector())
            + self.admission.as_ref().map_or(0, |admission| {
                WIRE_ADDRESS_LEN + 4 + admission.joiners().count() * 4 + 32
            })
    }

    /// Decodes one canonical frame, rejecting trailing bytes.
    ///
    /// The result remains a peer report. [`Epochs::deliver`](crate::metis::Epochs::deliver)
    /// re-validates lifecycle authority before it changes state.
    ///
    /// # Errors
    ///
    /// Returns [`DeclarationDecodeError`] for an unknown version, invalid
    /// length, impossible address, malformed rank, or malformed cut.
    pub fn from_bytes(bytes: &[u8]) -> Result<Self, DeclarationDecodeError> {
        if bytes.len() < DECLARATION_WIRE_MIN_LEN {
            return Err(DeclarationDecodeError::UnexpectedLength {
                expected: DECLARATION_WIRE_MIN_LEN,
                found: bytes.len(),
            });
        }
        let version = bytes[0];
        if version != DECLARATION_WIRE_V1 && version != DECLARATION_WIRE_V2 {
            return Err(DeclarationDecodeError::UnknownVersion(version));
        }
        let short = DeclarationDecodeError::UnexpectedLength {
            expected: DECLARATION_WIRE_MIN_LEN,
            found: bytes.len(),
        };
        let ((generation, dot), rest) = read_address_parts(&bytes[1..]).ok_or(short)?;
        let address = EpochAddress::try_from_parts(generation, dot)?;
        let stamp = rest.get(..KAIROS_WIRE_LEN).ok_or(short)?;
        let rank = Kairos::from_bytes(stamp).map_err(DeclarationDecodeError::Rank)?;
        let rest = &rest[KAIROS_WIRE_LEN..];
        let (cut, tail) = decode_cut(rest)?;
        let (admission, admission_commitment, tail) = if version == DECLARATION_WIRE_V2 {
            let (admission, rest) = read_admission(bytes, tail, address.generation())?;
            let commitment: [u8; 32] = rest
                .get(..32)
                .and_then(|bytes| bytes.try_into().ok())
                .ok_or(short)?;
            (Some(admission), Some(commitment), &rest[32..])
        } else {
            (None, None, tail)
        };
        if !tail.is_empty() {
            return Err(DeclarationDecodeError::UnexpectedLength {
                expected: bytes.len() - tail.len(),
                found: bytes.len(),
            });
        }
        Ok(Self {
            address,
            rank,
            cut: Cut::from_witnessed(cut),
            admission,
            admission_commitment,
        })
    }
}

/// Reads a version-2 frame's admission section: the base address, then a
/// strictly ascending, nonempty joiner set, re-proving the one relation a
/// declaration fixes. The base is the declared generation's
/// predecessor. Allocation stays bounded by the input: the joiner count
/// is checked against the bytes that would have to back it before the
/// loop runs.
fn read_admission<'a>(
    bytes: &[u8],
    rest: &'a [u8],
    declared: u64,
) -> Result<(Admission, &'a [u8]), DeclarationDecodeError> {
    let short = DeclarationDecodeError::UnexpectedLength {
        expected: bytes.len() + 1,
        found: bytes.len(),
    };
    let ((base_generation, base_dot), rest) = read_address_parts(rest).ok_or(short)?;
    let base = EpochAddress::try_from_parts(base_generation, base_dot)?;
    if base_generation.checked_add(1) != Some(declared) {
        return Err(DeclarationDecodeError::AdmissionBaseMismatch {
            base: base_generation,
            declaration: declared,
        });
    }
    let (count, mut rest) = read_u32(rest).ok_or(short)?;
    if count == 0 {
        return Err(DeclarationDecodeError::EmptyAdmission);
    }
    if (count as usize).saturating_mul(4) > rest.len() {
        return Err(short);
    }
    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 previous.is_some_and(|previous| joiner <= previous) {
            return Err(DeclarationDecodeError::NonAscendingJoiners {
                previous: previous.unwrap_or(0),
                found: joiner,
            });
        }
        previous = Some(joiner);
        let _ = joiners.insert(joiner);
        rest = tail;
    }
    let admission =
        Admission::new(base, joiners).map_err(|_refusal| DeclarationDecodeError::EmptyAdmission)?;
    Ok((admission, rest))
}