minerva 0.2.0

Causal ordering for distributed systems
//! The bootstrap admission door (S292): verified lineage proof to
//! recognizer horizon.
//!
//! The lifecycle machine has two restore doors with two distinct trust
//! postures. [`Epochs::rehydrate`](super::Epochs::rehydrate) installs a
//! *trusted local checkpoint*: the caller vouches for its own storage,
//! and the whole machine state rides, open window and all.
//! [`Epochs::bootstrap`] is the peer-evidence door: a joiner holds
//! sealed records that are *verified peer claims*, checked against an
//! authenticated checkpoint certificate in the consumer's layer, and
//! needs the duplicate-recognizer horizon those records spell without
//! ever holding the fleet's live authority. The door installs exactly
//! the recognizer state (the retained lineage and the next window
//! generation) and nothing else: no open window, no latched winner, no
//! adoption bit, and never the affine
//! [`Adopted`](super::Adopted) witness, which stays live-only and must
//! be re-earned through [`adopt`](super::Epochs::adopt) against current
//! stability evidence.
//!
//! # What the door re-proves
//!
//! A [`SealRecord`](super::SealRecord) decode is structural (framing,
//! canonical form, addresses), deliberately not semantic: the S273 rule
//! keeps decoded values report records, and every lifecycle invariant is
//! re-proven at the door that installs them. This door re-proves exactly
//! the machine invariants the epoch-ledger codec re-proves for its own
//! sealed entries (the refuse-impossible-combinations duty, stated once
//! there and mirrored here): every candidate and ledger dot is
//! roster-minted, the winner is among its candidates, and the ledger is
//! covered by the sealed join, plus two checks the ledger codec never
//! needed: generation homogeneity of the candidate set (the seal frame
//! spells candidates as full addresses, so a crafted frame can claim a
//! foreign generation the machine could never retire), and roster
//! containment of the sealed join's support (a live join is roster-only
//! because adoption rounds are roster-keyed, but
//! [`recognize`](super::Epochs::recognize) grants the duplicate verdict
//! off the join alone, so an installed foreign entry would absorb
//! non-roster traffic). Entry
//! consecutiveness is not re-proven: it is
//! [`LineageProofRecord`](super::LineageProofRecord)'s own construction
//! invariant, held by both of that type's doors. Candidate membership in
//! the ledger is deliberately *not* required, matching the ledger codec:
//! an adoption report's own-counter is caller data the machine records
//! without proof, so honest machines can seal states where a candidate
//! outruns the canonicalized ledger.

extern crate alloc;

use alloc::collections::VecDeque;
use core::num::{NonZeroU64, NonZeroUsize};

use super::wire::LineageProofRecord;
use super::{Epochs, SealedEpoch};
use crate::metis::dot::Dot;

/// The bootstrap door's typed refusals, each naming its evidence.
///
/// Every variant is a state no honest fleet's exported proof can carry,
/// so a refusal here is a verification-layer failure or a hostile
/// frame, never a repair case.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum EpochBootstrapError {
    /// The proof retains no seal. A bootstrap below no seal is the
    /// ordinary fresh-join path and its door is
    /// [`Epochs::new`](super::Epochs::new); admitting the empty proof
    /// here would spell a second, weaker door for the same claim.
    #[error("bootstrap proof is empty: a fresh fleet joins through `Epochs::new`")]
    EmptyProof,
    /// The proof retains more generations than the caller's declared
    /// horizon. Trimming to the newest suffix is the caller's visible
    /// act (the generation link to its carried state runs through the
    /// newest entry), never a silent truncation here.
    #[error(
        "bootstrap proof retains {generations} generations, past the declared horizon {horizon}"
    )]
    HorizonExceeded {
        /// The proof's retained generation count.
        generations: usize,
        /// The caller's declared retention horizon.
        horizon: usize,
    },
    /// The newest sealed generation sits at or directly below the
    /// integer ceiling, so no *sealable* next window generation exists:
    /// a wrapped successor would alias, and a window at `u64::MAX`
    /// itself can never seal
    /// ([`try_seal`](super::Epochs::try_seal)'s terminal refusal), so
    /// admitting it would hand the joiner a wedge. Checked rather than
    /// saturated (the ledger codec's posture for the same arithmetic).
    #[error("bootstrap proof ends at the generation ceiling: no next window generation exists")]
    CeilingGeneration,
    /// A retained candidate address claims a generation other than its
    /// seal's: one window seals one generation, so its candidates share
    /// the winner's.
    #[error("sealed generation {generation}: candidate claims foreign generation {found}")]
    CandidateOutsideGeneration {
        /// The seal's winner generation.
        generation: u64,
        /// The foreign generation the candidate address claims.
        found: u64,
    },
    /// A retained candidate or ledger dot was minted by a station
    /// outside the caller's roster, which the deliver door would have
    /// refused before it could ever be retained.
    #[error("sealed generation {generation}: station {station} is outside the roster")]
    ForeignStation {
        /// The seal's winner generation.
        generation: u64,
        /// The non-roster minting station.
        station: u32,
    },
    /// The seal's winner address is not among its own retired
    /// candidates: the seal retains the winner as a candidate, and a
    /// recognizer missing it could not absorb the winning declaration's
    /// own replay.
    #[error("sealed generation {generation}: winner {winner} is not among its candidates")]
    WinnerNotCandidate {
        /// The seal's winner generation.
        generation: u64,
        /// The winner's declaration dot.
        winner: Dot,
    },
    /// A ledger dot rides above the sealed join. The seal canonicalizes
    /// its ledger against the join (nothing above it was delivered
    /// everywhere), so an uncovered ledger dot is a claim no seal can
    /// make.
    #[error("sealed generation {generation}: ledger dot {dot} is above the sealed join")]
    UncoveredLedgerDot {
        /// The seal's winner generation.
        generation: u64,
        /// The uncovered ledger dot.
        dot: Dot,
    },
    /// A retained admission's base is not the entry's own predecessor:
    /// a boundary rides exactly the record sealed one past its base
    /// (PRD 0028 R1), so any other base is a claim no seal can make.
    #[error("sealed generation {generation}: the admission base is not the predecessor")]
    AdmissionBaseMismatch {
        /// The admission-bearing generation.
        generation: u64,
    },
    /// A retained admission's joiner is already a member at that
    /// generation --- admitted earlier, or a founding member --- which
    /// the live doors refuse before any seal could retain it.
    #[error("sealed generation {generation}: joiner {station} is already a member")]
    AdmissionAlreadyMember {
        /// The admission-bearing generation.
        generation: u64,
        /// The twice-admitted or founding-member joiner.
        station: u32,
    },
}

/// One retained entry's containment proof, against the roster of that
/// entry's own generation: candidate homogeneity and membership, the
/// winner among its candidates, ledger dots minted inside and covered by
/// the join, and the join's support inside the family. The join-support
/// clause is what keeps `recognize` from absorbing foreign traffic: it
/// grants the duplicate verdict off `sealed_join.get(dot.station())`
/// alone. A
/// zero-counter member is lawfully absent, so the reverse containment is
/// deliberately not required.
fn verify_entry_containment(
    seal: &super::wire::SealRecord,
    winner: super::gate::EpochAddress,
    generation: u64,
    historical: &alloc::collections::BTreeSet<u32>,
) -> Result<(), EpochBootstrapError> {
    for candidate in seal.candidates() {
        if candidate.generation() != generation {
            return Err(EpochBootstrapError::CandidateOutsideGeneration {
                generation,
                found: candidate.generation(),
            });
        }
        if !historical.contains(&candidate.declaration().station()) {
            return Err(EpochBootstrapError::ForeignStation {
                generation,
                station: candidate.declaration().station(),
            });
        }
    }
    if !seal.candidates().any(|candidate| candidate == winner) {
        return Err(EpochBootstrapError::WinnerNotCandidate {
            generation,
            winner: winner.declaration(),
        });
    }
    for dot in seal.declaration_dots() {
        if !historical.contains(&dot.station()) {
            return Err(EpochBootstrapError::ForeignStation {
                generation,
                station: dot.station(),
            });
        }
        if dot.counter() > seal.sealed_join().get(dot.station()) {
            return Err(EpochBootstrapError::UncoveredLedgerDot { generation, dot });
        }
    }
    if let Some((station, _)) = seal
        .sealed_join()
        .iter()
        .find(|(station, _)| !historical.contains(station))
    {
        return Err(EpochBootstrapError::ForeignStation {
            generation,
            station,
        });
    }
    Ok(())
}

impl Epochs {
    /// Constructs a joiner's lifecycle machine from a verified lineage
    /// proof: the recognizer horizon installed, the authority left
    /// where it lives.
    ///
    /// `roster` is the *oldest retained generation's* roster. For a proof with
    /// no admissions, that is the current roster, exactly as at
    /// [`new`](Self::new); duplicate roster ids collapse. The proof's retained
    /// admissions evolve it forward, so the machine derives its current roster
    /// rather than taking it as configuration (PRD 0028 R4). Membership comes
    /// from the proof; the caller grounds only where the retained history
    /// starts. `horizon` is the caller's configuration. The trust decision
    /// stays the caller's: this door never checks a certificate, and a proof
    /// should reach it only after the consumer's authenticated checkpoint
    /// verification. The door owns the machine contract. It re-proves the
    /// sealed-entry invariants (see the module record). It installs the
    /// entries as the retained lineage, oldest first. It opens the machine at
    /// the next window generation, the newest seal's successor. That matches
    /// the kind-9 law: the carried state directly succeeds the proof. The
    /// returned machine holds no window, no latched winner, and no adoption:
    /// old-addressed traffic answers through [`recognize`](Self::recognize)
    /// and the report doors identically to a machine that lived through the
    /// sealed generations, and native authority is re-earned through the
    /// ordinary rounds.
    ///
    /// # Errors
    ///
    /// The [`EpochBootstrapError`] vocabulary: the empty proof, the
    /// horizon overrun, the generation ceiling, and the per-entry
    /// machine-invariant refusals. The proof remains available to the
    /// caller on refusal.
    pub fn bootstrap(
        roster: impl IntoIterator<Item = u32>,
        horizon: NonZeroUsize,
        proof: &LineageProofRecord,
    ) -> Result<Self, EpochBootstrapError> {
        let roster: alloc::collections::BTreeSet<u32> = roster.into_iter().collect();
        let Some(newest) = proof.entries().last() else {
            return Err(EpochBootstrapError::EmptyProof);
        };
        if proof.len() > horizon.get() {
            return Err(EpochBootstrapError::HorizonExceeded {
                generations: proof.len(),
                horizon: horizon.get(),
            });
        }
        // The next window generation must itself be sealable: a window at
        // the `u64::MAX` ceiling can never seal (`try_seal`'s terminal
        // refusal, because its successor would alias its generation), so a
        // proof that would open one refuses here instead of wedging the
        // joiner (the review-round finding).
        // The successor of a one-based generation is one-based, so the
        // `NonZeroU64` step adds no refusal (ruling R-91).
        let Some(generation) = newest
            .seal()
            .declaration()
            .generation()
            .checked_add(1)
            .filter(|&next| next < u64::MAX)
            .and_then(NonZeroU64::new)
        else {
            return Err(EpochBootstrapError::CeilingGeneration);
        };
        // The proof's generations need not share one roster (PRD 0028
        // R4): each retained admission widens it for the generations
        // after its record. The caller's `roster` is therefore the
        // *oldest retained generation's* roster --- for a proof with no
        // admissions that is the current roster, exactly the old
        // contract --- and the walk widens it forward, so every
        // containment check below ranges over the roster *of that
        // entry's own generation*, a joiner's dots before its admission
        // refuse, and a crafted admission of a member the caller founded
        // with refuses as already present. Deriving the oldest roster by
        // subtracting the proof's own admissions would assume the fact
        // under proof; the caller's configuration is the independent
        // ground, and the machine's current roster is *derived* from it.
        let mut historical = roster;
        let mut lineage = VecDeque::with_capacity(proof.len());
        let mut previous: Option<super::gate::EpochAddress> = None;
        for entry in proof.entries() {
            let seal = entry.seal();
            let winner = seal.declaration();
            let generation = winner.generation();
            verify_entry_containment(seal, winner, generation, &historical)?;
            if let Some(admission) = seal.admission() {
                if admission.base().generation().checked_add(1) != Some(generation)
                    || previous.is_some_and(|previous| previous != admission.base())
                {
                    return Err(EpochBootstrapError::AdmissionBaseMismatch { generation });
                }
                if let Some(station) = admission
                    .joiners()
                    .find(|joiner| historical.contains(joiner))
                {
                    return Err(EpochBootstrapError::AdmissionAlreadyMember {
                        generation,
                        station,
                    });
                }
                historical.extend(admission.joiners());
            }
            previous = Some(winner);
            lineage.push_back(SealedEpoch {
                declaration: winner,
                candidates: seal.candidates().collect(),
                protocol: seal.declaration_dots().collect(),
                sealed_join: seal.sealed_join().clone(),
                admission: seal.admission().cloned(),
            });
        }
        Ok(Self {
            roster: historical,
            horizon,
            generation,
            window: None,
            lineage,
            arrival: None,
        })
    }
}