minerva 0.2.0

Causal ordering for distributed systems
extern crate alloc;

use alloc::vec::Vec;
use thiserror::Error;

use super::seal::{SEAL_WIRE_MIN_LEN, SealDecodeBudget, SealDecodeError, SealRecord};
use super::shared::read_u32;

/// Version tag for the canonical lineage-proof wire encoding.
const LINEAGE_PROOF_WIRE_V1: u8 = 0x01;
/// Version tag for the admission-bearing sibling encoding (PRD 0028 R7,
/// ruling R-89): the version-1 layout whose embedded seals may spell
/// version 2. Emitted exactly when any entry carries an admission, so a
/// version-1 frame embeds only version-1 seals. This is the frozen v1
/// contract. A version-1 reader refuses a heterogeneous proof
/// whole at its first byte.
const LINEAGE_PROOF_WIRE_V2: u8 = 0x02;
/// Version and entry count.
const LINEAGE_PROOF_WIRE_HEADER_LEN: usize = 5;
/// The opaque boundary-node digest width.
const LINEAGE_NODE_LEN: usize = 32;
/// One node plus the smallest possible embedded seal.
const LINEAGE_PROOF_ENTRY_MIN_LEN: usize = LINEAGE_NODE_LEN + SEAL_WIRE_MIN_LEN;

/// Ceilings for what a [`LineageProofRecord`] decoder may materialize.
///
/// The generation ceiling applies to the proof, and the candidate and
/// admission-joiner ceilings are threaded unchanged into every embedded
/// seal.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct LineageProofDecodeBudget {
    generations: usize,
    candidates: usize,
    joiners: usize,
}

impl LineageProofDecodeBudget {
    /// Builds generation and per-seal candidate ceilings, with the seal
    /// codec's default admission-joiner ceiling.
    #[must_use]
    pub const fn new(max_generations: usize, max_candidates: usize) -> Self {
        Self {
            generations: max_generations,
            candidates: max_candidates,
            joiners: SealDecodeBudget::new(0).max_joiners(),
        }
    }

    /// Replaces the per-seal admission-joiner ceiling.
    #[must_use]
    pub const fn with_max_joiners(self, max_joiners: usize) -> Self {
        Self {
            joiners: max_joiners,
            ..self
        }
    }

    /// Maximum sealed generations.
    #[must_use]
    pub const fn max_generations(self) -> usize {
        self.generations
    }

    /// Per-set candidate ceiling for every embedded seal.
    #[must_use]
    pub const fn max_candidates(self) -> usize {
        self.candidates
    }
}

/// Decode and construction failure for a [`LineageProofRecord`].
#[non_exhaustive]
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineageProofDecodeError {
    /// The leading version byte is not recognized.
    #[error("unknown lineage-proof wire version: {0:#04x}")]
    UnknownVersion(u8),
    /// A version-1 proof embeds an admission-bearing seal: the frozen
    /// version-1 contract embeds only version-1 seals, and the canonical
    /// spelling for a heterogeneous proof is the version-2 sibling.
    #[error("lineage-proof v1 embeds an admission-bearing seal at generation {generation}")]
    AdmissionInVersionOne {
        /// The admission-bearing generation.
        generation: u64,
    },
    /// A version-2 proof with no admission-bearing seal: the version-1
    /// spelling is the canonical one, so this frame is non-canonical by
    /// construction.
    #[error("lineage-proof v2 carries no admission-bearing seal")]
    VersionTwoWithoutAdmissions,
    /// The input is too short for its count or carries trailing bytes.
    #[error("unexpected lineage-proof frame length: expected {expected}, found {found}")]
    UnexpectedLength {
        /// Required lower bound, or exact length for trailing input.
        expected: usize,
        /// Supplied input length.
        found: usize,
    },
    /// The entry count exceeds the caller's generation budget.
    #[error("lineage proof declares {count} generations, past the budget's {budget}")]
    TooManyGenerations {
        /// Declared entry count.
        count: u64,
        /// Caller generation budget.
        budget: u64,
    },
    /// Entries are not consecutive ascending generations.
    #[error("lineage proof generations not consecutive: {found} after {previous}")]
    NonConsecutiveGenerations {
        /// Preceding generation.
        previous: u64,
        /// Offending generation.
        found: u64,
    },
    /// An embedded seal refused.
    #[error("lineage proof seal entry: {0}")]
    Seal(#[from] SealDecodeError),
}

/// Validates strict successor generations without wrapping at `u64::MAX`.
const fn consecutive(
    previous: Option<u64>,
    generation: u64,
) -> Result<(), LineageProofDecodeError> {
    match previous {
        None => Ok(()),
        Some(previous) => match previous.checked_add(1) {
            Some(next) if next == generation => Ok(()),
            _ => Err(LineageProofDecodeError::NonConsecutiveGenerations {
                previous,
                found: generation,
            }),
        },
    }
}

/// One retained lineage entry: a sealed epoch record beside its opaque
/// boundary-node chain value.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LineageProofEntry {
    seal: SealRecord,
    node: [u8; LINEAGE_NODE_LEN],
}

impl LineageProofEntry {
    /// Builds one entry from a seal record and boundary-node value.
    #[must_use]
    pub const fn new(seal: SealRecord, node: [u8; LINEAGE_NODE_LEN]) -> Self {
        Self { seal, node }
    }

    /// The sealed epoch record.
    #[must_use]
    pub const fn seal(&self) -> &SealRecord {
        &self.seal
    }

    /// The boundary-node chain value.
    #[must_use]
    pub const fn node(&self) -> [u8; LINEAGE_NODE_LEN] {
        self.node
    }
}

/// The bootstrap lineage proof in wire shape (S284; the Minerva half of
/// the PRD 0025 kind-9 payload).
///
/// Entries are consecutive generations in oldest-first order. Trust and
/// installation remain outside the codec at the consumer checkpoint door
/// and [`Epochs::bootstrap`](crate::metis::Epochs::bootstrap).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LineageProofRecord {
    entries: Vec<LineageProofEntry>,
}

impl LineageProofRecord {
    /// Builds a proof from consecutive oldest-first entries. An empty
    /// proof is valid for a fresh fleet.
    ///
    /// # Errors
    ///
    /// Returns [`LineageProofDecodeError::NonConsecutiveGenerations`]
    /// when an entry is not its predecessor's exact successor.
    pub fn try_new(entries: Vec<LineageProofEntry>) -> Result<Self, LineageProofDecodeError> {
        let mut previous: Option<u64> = None;
        for entry in &entries {
            let generation = entry.seal.declaration().generation();
            consecutive(previous, generation)?;
            previous = Some(generation);
        }
        Ok(Self { entries })
    }

    /// The retained entries, oldest first.
    #[must_use]
    pub fn entries(&self) -> &[LineageProofEntry] {
        &self.entries
    }

    /// The retained generation count.
    #[must_use]
    pub const fn len(&self) -> usize {
        self.entries.len()
    }

    /// Whether the proof is empty.
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    /// Whether any entry's seal carries an admission: the version-2
    /// tag's exact condition, so each version keeps its own canonical
    /// bijection.
    fn recorded(&self) -> bool {
        self.entries
            .iter()
            .any(|entry| entry.seal.admission().is_some())
    }

    /// Encodes this proof as its canonical wire frame: version 1, or the
    /// version-2 sibling exactly when any entry's seal carries an
    /// admission, so a nominally version-1 proof never embeds a
    /// version-2 seal.
    #[must_use]
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut out = Vec::with_capacity(self.encoded_len());
        out.push(if self.recorded() {
            LINEAGE_PROOF_WIRE_V2
        } else {
            LINEAGE_PROOF_WIRE_V1
        });
        let count = u32::try_from(self.entries.len()).unwrap_or(u32::MAX);
        out.extend_from_slice(&count.to_be_bytes());
        for entry in &self.entries {
            out.extend_from_slice(&entry.node);
            out.extend_from_slice(&entry.seal.to_bytes());
        }
        out
    }

    /// The exact frame length, computed allocation-free.
    #[must_use]
    pub fn encoded_len(&self) -> usize {
        self.entries
            .iter()
            .fold(LINEAGE_PROOF_WIRE_HEADER_LEN, |length, entry| {
                length + LINEAGE_NODE_LEN + entry.seal.encoded_len()
            })
    }

    /// Decodes one canonical frame under the input-length ceiling.
    ///
    /// # Errors
    ///
    /// Returns [`LineageProofDecodeError`] for malformed structure or
    /// content.
    pub fn from_bytes(bytes: &[u8]) -> Result<Self, LineageProofDecodeError> {
        Self::decode(bytes, None)
    }

    /// Decodes one canonical frame under caller-owned generation and
    /// per-seal retained-set budgets.
    ///
    /// # Errors
    ///
    /// Returns [`LineageProofDecodeError`] for budget, structure, or
    /// content refusals.
    pub fn from_bytes_with_budget(
        bytes: &[u8],
        budget: LineageProofDecodeBudget,
    ) -> Result<Self, LineageProofDecodeError> {
        Self::decode(bytes, Some(budget))
    }

    fn decode(
        bytes: &[u8],
        budget: Option<LineageProofDecodeBudget>,
    ) -> Result<Self, LineageProofDecodeError> {
        if bytes.len() < LINEAGE_PROOF_WIRE_HEADER_LEN {
            return Err(LineageProofDecodeError::UnexpectedLength {
                expected: LINEAGE_PROOF_WIRE_HEADER_LEN,
                found: bytes.len(),
            });
        }
        let version = bytes[0];
        if version != LINEAGE_PROOF_WIRE_V1 && version != LINEAGE_PROOF_WIRE_V2 {
            return Err(LineageProofDecodeError::UnknownVersion(version));
        }
        let recorded = version == LINEAGE_PROOF_WIRE_V2;
        let (count, rest) =
            read_u32(&bytes[1..]).ok_or(LineageProofDecodeError::UnexpectedLength {
                expected: LINEAGE_PROOF_WIRE_HEADER_LEN,
                found: bytes.len(),
            })?;
        if let Some(budget) = budget
            && u64::from(count) > budget.max_generations() as u64
        {
            return Err(LineageProofDecodeError::TooManyGenerations {
                count: u64::from(count),
                budget: budget.max_generations() as u64,
            });
        }
        if count as usize > rest.len() / LINEAGE_PROOF_ENTRY_MIN_LEN {
            let expected = (count as usize)
                .saturating_mul(LINEAGE_PROOF_ENTRY_MIN_LEN)
                .saturating_add(LINEAGE_PROOF_WIRE_HEADER_LEN);
            return Err(LineageProofDecodeError::UnexpectedLength {
                expected,
                found: bytes.len(),
            });
        }

        let mut entries = Vec::with_capacity(count as usize);
        let mut previous: Option<u64> = None;
        let mut rest = rest;
        for _ in 0..count {
            let node: [u8; LINEAGE_NODE_LEN] = rest
                .get(..LINEAGE_NODE_LEN)
                .and_then(|node| node.try_into().ok())
                .ok_or(LineageProofDecodeError::UnexpectedLength {
                    expected: bytes.len() - rest.len() + LINEAGE_PROOF_ENTRY_MIN_LEN,
                    found: bytes.len(),
                })?;
            rest = &rest[LINEAGE_NODE_LEN..];
            let (seal, tail) = SealRecord::from_prefix_with_budget(
                rest,
                budget.map(|budget| {
                    SealDecodeBudget::new(budget.max_candidates()).with_max_joiners(budget.joiners)
                }),
            )?;
            rest = tail;
            // The inner-version rule: a version-1 proof embeds only
            // version-1 seals (the frozen contract a v1-negotiated peer
            // decodes whole), and the version-2 spelling is canonical ---
            // used exactly when an admission rides.
            if !recorded && seal.admission().is_some() {
                return Err(LineageProofDecodeError::AdmissionInVersionOne {
                    generation: seal.declaration().generation(),
                });
            }
            let generation = seal.declaration().generation();
            consecutive(previous, generation)?;
            previous = Some(generation);
            entries.push(LineageProofEntry { seal, node });
        }
        if recorded && !entries.iter().any(|entry| entry.seal.admission().is_some()) {
            return Err(LineageProofDecodeError::VersionTwoWithoutAdmissions);
        }

        if rest.is_empty() {
            Ok(Self { entries })
        } else {
            Err(LineageProofDecodeError::UnexpectedLength {
                expected: bytes.len() - rest.len(),
                found: bytes.len(),
            })
        }
    }
}