minerva 0.2.0

Causal ordering for distributed systems
//! Canonical persistence for the epoch lifecycle record.

extern crate alloc;

mod codec;
mod error;
mod state;

use alloc::collections::BTreeSet;
use core::num::NonZeroUsize;

pub use error::{EpochLedgerDecodeBudget, EpochLedgerDecodeError, EpochLedgerRehydrateError};
use state::SnapshotState;

use super::Epochs;

/// An inert, validated record of an [`Epochs`] lifecycle machine.
///
/// Decoding bytes yields this record, never an `Epochs`, an
/// [`Adopted`](crate::metis::Adopted), or a
/// [`SealedEpoch`](crate::metis::SealedEpoch). This distinction is the trust
/// boundary: peer bytes are evidence a caller may compare or retain, while
/// only [`Epochs::rehydrate`] explicitly installs a trusted local checkpoint
/// into a new machine. The type is opaque so decoded state cannot be edited
/// into a stronger claim before that compatibility check.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EpochLedgerSnapshot {
    state: SnapshotState,
}

impl EpochLedgerSnapshot {
    /// The next window's lineage generation.
    #[must_use]
    pub const fn generation(&self) -> u64 {
        self.state.generation.get()
    }

    /// The roster recorded by the checkpoint --- the checkpointed
    /// generation's, admissions included --- in ascending order.
    pub fn roster(&self) -> impl Iterator<Item = u32> + '_ {
        self.state.roster.iter().copied()
    }

    /// The retained-recognizer horizon recorded by the checkpoint.
    #[must_use]
    pub const fn horizon(&self) -> u64 {
        self.state.horizon
    }

    /// Whether the checkpoint preserves an open lifecycle window.
    #[must_use]
    pub const fn has_open_window(&self) -> bool {
        self.state.window.is_some()
    }

    /// Number of retained sealed recognizers.
    #[must_use]
    pub const fn sealed_len(&self) -> usize {
        self.state.lineage.len()
    }

    /// Number of candidates in the open window, or zero when closed.
    #[must_use]
    pub fn candidate_len(&self) -> usize {
        self.state
            .window
            .as_ref()
            .map_or(0, |window| window.candidates.len())
    }
}

impl Epochs {
    /// Captures this machine's complete lifecycle state as an inert snapshot.
    ///
    /// In-flight confirmation and adoption reports ride whole. They are
    /// idempotent receipts whose loss is repairable, but retaining them makes a
    /// clean restart exact and bounded by the same roster and candidate limits
    /// as the live machine. The affine [`Adopted`](crate::metis::Adopted)
    /// witness does not ride: [`rehydrate`](Self::rehydrate) restores the local
    /// adoption bit and rounds, and the caller re-earns authority by calling
    /// [`adopt`](Self::adopt) against its current stability evidence.
    #[must_use]
    pub fn snapshot(&self) -> EpochLedgerSnapshot {
        EpochLedgerSnapshot {
            state: SnapshotState::from_epochs(self),
        }
    }

    /// Rehydrates a new lifecycle machine from a trusted local snapshot.
    ///
    /// `roster` and `horizon` are the caller's current configuration, not
    /// values silently accepted from storage. Both must match the checkpoint
    /// exactly before any state is installed. Ordinary peer bytes should stay
    /// an [`EpochLedgerSnapshot`] and enter the live machine only through its
    /// report doors; this constructor is the explicit local-checkpoint trust
    /// decision.
    ///
    /// # Errors
    ///
    /// [`EpochLedgerRehydrateError::RosterMismatch`] or
    /// [`EpochLedgerRehydrateError::HorizonMismatch`] when the current
    /// configuration differs from the checkpoint. The snapshot remains
    /// available to the caller on refusal.
    pub fn rehydrate(
        snapshot: &EpochLedgerSnapshot,
        roster: impl IntoIterator<Item = u32>,
        horizon: NonZeroUsize,
    ) -> Result<Self, EpochLedgerRehydrateError> {
        let roster: BTreeSet<u32> = roster.into_iter().collect();
        if roster != snapshot.state.roster {
            return Err(EpochLedgerRehydrateError::RosterMismatch);
        }
        let configured = u64::try_from(horizon.get()).unwrap_or(u64::MAX);
        if configured != snapshot.state.horizon {
            return Err(EpochLedgerRehydrateError::HorizonMismatch {
                checkpoint: snapshot.state.horizon,
                configured,
            });
        }
        Ok(snapshot.state.to_epochs(horizon))
    }
}