minerva 0.2.0

Causal ordering for distributed systems
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
//! The arrival round, held in the machine: PRD 0028's carriage of a
//! membership admission into the sealed record (S342, ruling R-89).
//!
//! An [`Admission`] is a boundary: one sealed base and the whole set of
//! stations joining after it. The incumbency agrees it through a round of
//! endorsements the machine itself holds ([`ArrivalRound`]), and the agreed
//! boundary travels in exactly one carrier: the winning declaration.
//! [`Epochs::declare`](super::Epochs::declare) tags its declaration with
//! the round's boundary exactly when the machine holds the round complete,
//! and a sealed record carries what its winner declared, or nothing.
//!
//! Why the winner, stated once (R-89, and argued in PRD 0028 R2): content
//! that must be agreed at a seal has two lawful carriers in this machine:
//! a value the rounds *force* (the join, R-86), or a value whose
//! absence *blocks* (the winner: no seal completes without the latched
//! winner's full adoption round). A per-member admission tag would do
//! neither. A spoken `none` completes a round, so a departure
//! attestation applied at one replica and not yet at another could split
//! the answer. A joiner set folds in no lattice, so no forcing argument
//! exists, and the winner is the only carrier left. The rounds'
//! substitution ([`Round::join_when_complete`](super::Round)) is untouched
//! by this module: nothing the admission reads passes through a
//! substitutable slot.
//!
//! The round deliberately has **no substitution** of its own. A departure
//! attestation that lands while a round is open freezes that round. Its
//! family is fixed at opening, and an attested member never endorses. The
//! arrival re-agrees over a later base. Substituting an endorsement
//! would re-import the racing-departure schedule this design closes.
//!
//! The round is pinned to its base. Every seal ends it: a seal that carries
//! its boundary consumes it, and any other seal supersedes its base, so a
//! stale endorsement is structurally unspendable and no freshness check
//! exists anywhere. Prevents-or-reports, per refusal: the doors here
//! *prevent*. Each guards the first irreversible act on this replica's
//! own state. The one report-grade surface is named at
//! [`ArrivalRefusal::DiscordantBase`], which is fork evidence about a peer.

extern crate alloc;

use alloc::collections::{BTreeMap, BTreeSet};

use super::super::stability::Stability;
use super::gate::EpochAddress;
use super::vouched::Vouched;
use super::{Epochs, SealedEpoch};

/// One membership boundary: the sealed base and the stations joining at
/// every generation after it.
///
/// The value the arrival round agrees and the winning declaration carries.
/// Nonempty by construction; the base names the record whose successor
/// window may carry this admission, so a seal past the base retires every
/// object holding one.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Admission {
    base: EpochAddress,
    joiners: BTreeSet<u32>,
}

impl Admission {
    /// Builds a boundary over a nonempty joiner set.
    ///
    /// # Errors
    ///
    /// [`ArrivalRefusal::EmptyAdmission`] for an empty joiner set: a
    /// boundary that admits nobody is not a boundary, and refusing it here
    /// keeps every later door's evidence about a station rather than about
    /// nothing.
    pub fn new(
        base: EpochAddress,
        joiners: impl IntoIterator<Item = u32>,
    ) -> Result<Self, ArrivalRefusal> {
        let joiners: BTreeSet<u32> = joiners.into_iter().collect();
        if joiners.is_empty() {
            return Err(ArrivalRefusal::EmptyAdmission);
        }
        Ok(Self { base, joiners })
    }

    /// The sealed base: the record whose successor the joiners enter at.
    #[must_use]
    pub const fn base(&self) -> EpochAddress {
        self.base
    }

    /// The joining stations, ascending.
    pub fn joiners(&self) -> impl Iterator<Item = u32> + '_ {
        self.joiners.iter().copied()
    }

    /// Whether `station` is among the joiners.
    #[must_use]
    pub fn admits(&self, station: u32) -> bool {
        self.joiners.contains(&station)
    }

    /// The joiner set, for the seal's roster extension.
    pub(super) const fn joiner_set(&self) -> &BTreeSet<u32> {
        &self.joiners
    }
}

/// One incumbent's endorsement of a boundary, as it travels.
///
/// The claim half of a [`Vouched`] report: the transport authenticates the
/// speaker, [`Vouched::trust`] binds the claim to it, and
/// [`Epochs::fold_endorsement`] latches the slot. Carrying the whole
/// boundary, both base and joiners, stops one round's words from
/// folding into another (PRD 0027 R3b's `ForeignBoundary`).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Endorsement {
    admission: Admission,
    commitment: [u8; 32],
}

impl Endorsement {
    /// Rebuilds a peer's endorsement from its transported parts.
    ///
    /// The claim half of the trust seam: the wire carries the boundary
    /// and commitment, the caller's transport authenticates the speaker, and
    /// [`Vouched::trust`] binds the rebuilt claim to it --- the same
    /// posture as every report this crate takes.
    #[must_use]
    pub const fn from_parts(admission: Admission, commitment: [u8; 32]) -> Self {
        Self {
            admission,
            commitment,
        }
    }

    /// The endorsed boundary.
    #[must_use]
    pub const fn admission(&self) -> &Admission {
        &self.admission
    }

    /// The endorsed record commitment: the caller-supplied 32-byte value
    /// over the base's exact seal frame (PRD 0028 R7). Opaque here, like
    /// the lineage node: minerva compares equality and refuses discord;
    /// what it is a hash *of* stays the caller's.
    #[must_use]
    pub const fn commitment(&self) -> &[u8; 32] {
        &self.commitment
    }
}

/// The machine-held arrival round: one boundary, one endorsement slot per
/// family member, latched.
///
/// Complete when every slot holds, which is the tagging license
/// [`Epochs::declare`](super::Epochs::declare) reads and the assent
/// [`Epochs::adopt`](super::Epochs::adopt) requires. The family is fixed
/// at opening from the tracker's surviving members. It never widens and
/// never substitutes.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArrivalRound {
    pub(super) admission: Admission,
    /// The record commitment the round agrees beside the boundary
    /// (PRD 0028 R7): fixed at opening, discord refused at every fold.
    /// An `EpochAddress` is a winner, not a digest, so the address
    /// concord catches a forked winner and passes a forked record under
    /// one winner; the commitment closes exactly that gap.
    pub(super) commitment: [u8; 32],
    /// One slot per family member: `true` once that member's endorsement
    /// folded. Latched; a repeat absorbs.
    pub(super) slots: BTreeMap<u32, bool>,
    pub(super) own: u32,
}

impl ArrivalRound {
    /// The boundary this round agrees.
    #[must_use]
    pub const fn admission(&self) -> &Admission {
        &self.admission
    }

    /// This replica's own station.
    #[must_use]
    pub const fn own(&self) -> u32 {
        self.own
    }

    /// The family the round ranges over, ascending.
    pub fn family(&self) -> impl Iterator<Item = u32> + '_ {
        self.slots.keys().copied()
    }

    /// The family members whose endorsements have not folded, ascending:
    /// who is holding the admission up.
    pub fn pending(&self) -> impl Iterator<Item = u32> + '_ {
        self.slots
            .iter()
            .filter(|&(_, &endorsed)| !endorsed)
            .map(|(&station, _)| station)
    }

    /// Whether every family member has endorsed: the tagging license.
    #[must_use]
    pub fn complete(&self) -> bool {
        self.slots.values().all(|&endorsed| endorsed)
    }

    /// The record commitment the round agrees (PRD 0028 R7).
    #[must_use]
    pub const fn commitment(&self) -> &[u8; 32] {
        &self.commitment
    }
}

/// The arrival round's typed refusals. Every variant names its evidence.
///
/// `#[non_exhaustive]` so a future refusal is additive.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum ArrivalRefusal {
    /// An admission with no joiners. Prevents: nothing opens.
    #[error("empty admission: a boundary must admit at least one station")]
    EmptyAdmission,
    /// A joiner already on the roster. Prevents --- and because departure
    /// never removes a station from the roster (R-84, R-87), this is the
    /// door that forces incarnation: a returning member returns as a new
    /// station or not at all.
    #[error("already present: station {station} is on the roster")]
    AlreadyPresent {
        /// The joining station already present.
        station: u32,
    },
    /// The opener or an endorser is outside the round's family. Prevents.
    #[error("unknown station: {station} is not in the round's family")]
    UnknownStation {
        /// The station outside the family.
        station: u32,
    },
    /// A folded report carrying this replica's own station. Prevents: own
    /// words enter at [`Epochs::open_arrival`], never as a peer report.
    #[error("impersonated: a report may not fill this replica's own slot ({station})")]
    Impersonated {
        /// The impersonated station.
        station: u32,
    },
    /// The base is ahead of this machine. Prevents, and **recoverable**:
    /// hold the boundary and retry once this machine seals the base.
    #[error("premature: base generation {base} is ahead of newest sealed {newest}")]
    Premature {
        /// The offered base's generation.
        base: u64,
        /// The newest sealed generation, zero when nothing is sealed.
        newest: u64,
    },
    /// The base is behind this machine's newest seal. Prevents, and
    /// terminal: agree the arrival again over a base the fleet has not
    /// passed.
    #[error("elapsed: base generation {base} is behind newest sealed {newest}")]
    Elapsed {
        /// The offered base's generation.
        base: u64,
        /// The newest sealed generation.
        newest: u64,
    },
    /// No epoch is sealed, so no boundary can be founded. Prevents.
    #[error("unfounded: no sealed epoch exists to found a boundary on")]
    Unfounded,
    /// The base names this machine's newest generation at a different
    /// winner. Reports: this is fork evidence about the speaker, not a
    /// mistake to absorb --- two honest members cannot hold two records at
    /// one generation.
    #[error("discordant base: held {held:?}, offered {offered:?}")]
    DiscordantBase {
        /// This machine's newest sealed address.
        held: EpochAddress,
        /// The offered base.
        offered: EpochAddress,
    },
    /// A second boundary while a round holds another. Prevents: at most
    /// one round per base, and a competing admission is refused loudly
    /// with both boundaries' bases in evidence rather than reconciled.
    #[error("discordant round: a round is open over base {held:?}, offered {offered:?}")]
    DiscordantRound {
        /// The held round's base.
        held: EpochAddress,
        /// The offered boundary's base.
        offered: EpochAddress,
    },
    /// An endorsement naming the held round's base but another joiner set.
    /// Prevents: one round's words cannot fold into another
    /// (PRD 0027 R3b).
    #[error("foreign boundary: the endorsement names another joiner set over base {base:?}")]
    ForeignBoundary {
        /// The shared base both boundaries name.
        base: EpochAddress,
    },
    /// A peer endorsement with no round open. Prevents: opening is the
    /// caller's policy decision, so the machine refuses rather than
    /// joining a round nobody here agreed to run.
    #[error("no round: no arrival round is open")]
    NoRound,
    /// An endorsement whose record commitment differs from the held
    /// round's. Reports, and it is the concord's stronger reading
    /// (PRD 0028 R7): an `EpochAddress` is a winner, not a digest, so
    /// same-base-different-commitment is evidence of a forked record
    /// under one winner --- the divergence the address concord cannot
    /// see.
    #[error("commitment discord over base {base:?}")]
    CommitmentDiscord {
        /// The shared base both endorsements name.
        base: EpochAddress,
    },
    /// A re-open naming a different own station than the held round's.
    /// Prevents: one machine speaks with one voice per round, and the
    /// adoption assent binds to exactly that voice.
    #[error("discordant voice: the round is held by {held}, offered by {offered}")]
    DiscordantVoice {
        /// The held round's own station.
        held: u32,
        /// The offered opener.
        offered: u32,
    },
    /// An endorsement attributed to a station whose departure this tracker
    /// holds attested. What this prevents, and the `Departure::admits` posture
    /// at this door: a departed identity's words are not spendable. An
    /// attestation landing while a round is open therefore freezes the round,
    /// exactly where that member's word is missing. The slot can never fill,
    /// and the arrival re-agrees over a later base with the surviving family.
    #[error("attested departure: station {station} left and its words are not spendable")]
    AttestedDeparture {
        /// The attested-departed speaker.
        station: u32,
    },
}

impl Epochs {
    /// Opens the arrival round over `admission` and endorses it in the
    /// same act, returning the [`Endorsement`] to travel.
    ///
    /// One act, not two: an open round always carries its opener's word, so
    /// "round exists with this boundary" *is* this replica's assent, which is
    /// exactly what [`adopt`](Self::adopt) reads. The family is the tracker's
    /// surviving members, fixed here. The round never asks an attested or
    /// abandoned station. A departure attested after opening freezes the
    /// round, because its slot can never fold. That freeze costs the arrival a
    /// re-agreement over a later base, and costs the epoch machinery nothing.
    ///
    /// Idempotent for the identical boundary: the endorsement is re-minted
    /// and no state moves. Durability is the caller's mint-then-act duty
    /// (PRD 0028 R3): journal the endorsement before it travels.
    ///
    /// # Errors
    ///
    /// [`ArrivalRefusal::AlreadyPresent`] for a joiner on the roster
    /// (first: presence is the question a re-admission asks);
    /// [`ArrivalRefusal::UnknownStation`] for an opener outside the
    /// surviving family; [`ArrivalRefusal::Unfounded`],
    /// [`ArrivalRefusal::Premature`], [`ArrivalRefusal::Elapsed`], or
    /// [`ArrivalRefusal::DiscordantBase`] locating the base against this
    /// machine's lineage; [`ArrivalRefusal::DiscordantRound`] while a
    /// round holds another base;
    /// [`ArrivalRefusal::DiscordantVoice`] for a re-open under another own
    /// station; [`ArrivalRefusal::ForeignBoundary`] for another joiner set
    /// over the held base; [`ArrivalRefusal::CommitmentDiscord`] for the
    /// held boundary under another record commitment.
    pub fn open_arrival(
        &mut self,
        admission: Admission,
        commitment: [u8; 32],
        own: u32,
        stability: &Stability,
    ) -> Result<Endorsement, ArrivalRefusal> {
        if let Some(&station) = admission
            .joiner_set()
            .iter()
            .find(|station| self.roster.contains(station))
        {
            return Err(ArrivalRefusal::AlreadyPresent { station });
        }
        let family: BTreeSet<u32> = stability
            .surviving_family()
            .filter(|station| self.roster.contains(station))
            .collect();
        if !family.contains(&own) {
            return Err(ArrivalRefusal::UnknownStation { station: own });
        }
        self.locate_base(admission.base())?;
        if let Some(round) = &self.arrival {
            if round.own != own {
                return Err(ArrivalRefusal::DiscordantVoice {
                    held: round.own,
                    offered: own,
                });
            }
            if *round.admission() != admission {
                // The same distinction the folding door draws: another
                // joiner set over the held base is a foreign boundary;
                // another base is a competing round.
                if round.admission().base() == admission.base() {
                    return Err(ArrivalRefusal::ForeignBoundary {
                        base: admission.base(),
                    });
                }
                return Err(ArrivalRefusal::DiscordantRound {
                    held: round.admission().base(),
                    offered: admission.base(),
                });
            }
            if round.commitment != commitment {
                return Err(ArrivalRefusal::CommitmentDiscord {
                    base: admission.base(),
                });
            }
            return Ok(Endorsement {
                admission,
                commitment,
            });
        }
        let slots = family
            .iter()
            .map(|&station| (station, station == own))
            .collect();
        self.arrival = Some(ArrivalRound {
            admission: admission.clone(),
            commitment,
            slots,
            own,
        });
        Ok(Endorsement {
            admission,
            commitment,
        })
    }

    /// Folds a peer's endorsement into the open round. Latched; a repeat
    /// absorbs.
    ///
    /// # Errors
    ///
    /// [`ArrivalRefusal::NoRound`] with no round open;
    /// [`ArrivalRefusal::ForeignBoundary`] for another joiner set over the
    /// held base; [`ArrivalRefusal::DiscordantRound`] for another base
    /// (fork-shaped competing round evidence);
    /// [`ArrivalRefusal::CommitmentDiscord`] for the held base and joiner
    /// set under another record commitment (forked-record evidence,
    /// PRD 0028 R7);
    /// [`ArrivalRefusal::Impersonated`] for a report carrying this
    /// replica's own station; [`ArrivalRefusal::AttestedDeparture`] for a
    /// speaker whose departure the tracker holds attested --- a delayed
    /// word from a departed identity is refused, not latched, so the
    /// freeze this module's header states is enforced rather than
    /// documented; [`ArrivalRefusal::UnknownStation`] for a
    /// speaker outside the round's family --- a joiner included: nobody
    /// endorses itself in (PRD 0027 R1).
    pub fn fold_endorsement(
        &mut self,
        endorsement: &Vouched<Endorsement>,
        stability: &Stability,
    ) -> Result<(), ArrivalRefusal> {
        let by = endorsement.by();
        let offered = endorsement.claim().admission();
        if stability.attested(by).is_some() {
            return Err(ArrivalRefusal::AttestedDeparture { station: by });
        }
        let Some(round) = &mut self.arrival else {
            return Err(ArrivalRefusal::NoRound);
        };
        if offered.base() != round.admission.base() {
            return Err(ArrivalRefusal::DiscordantRound {
                held: round.admission.base(),
                offered: offered.base(),
            });
        }
        if *offered != round.admission {
            return Err(ArrivalRefusal::ForeignBoundary {
                base: round.admission.base(),
            });
        }
        if *endorsement.claim().commitment() != round.commitment {
            return Err(ArrivalRefusal::CommitmentDiscord {
                base: round.admission.base(),
            });
        }
        if by == round.own {
            return Err(ArrivalRefusal::Impersonated { station: by });
        }
        let Some(slot) = round.slots.get_mut(&by) else {
            return Err(ArrivalRefusal::UnknownStation { station: by });
        };
        *slot = true;
        Ok(())
    }

    /// The open arrival round, or `None`: the observability read, and the
    /// caller's completion poll before declaring.
    #[must_use]
    pub const fn arrival(&self) -> Option<&ArrivalRound> {
        self.arrival.as_ref()
    }

    /// Locates a boundary's base against this machine's own lineage: the
    /// PRD 0027 R3 ladder, one recoverable rung and three terminal ones.
    fn locate_base(&self, base: EpochAddress) -> Result<(), ArrivalRefusal> {
        let Some(newest) = self.lineage.back().map(SealedEpoch::declaration) else {
            return Err(ArrivalRefusal::Unfounded);
        };
        if base == newest {
            return Ok(());
        }
        if base.generation() > newest.generation() {
            return Err(ArrivalRefusal::Premature {
                base: base.generation(),
                newest: newest.generation(),
            });
        }
        if base.generation() == newest.generation() {
            return Err(ArrivalRefusal::DiscordantBase {
                held: newest,
                offered: base,
            });
        }
        Err(ArrivalRefusal::Elapsed {
            base: base.generation(),
            newest: newest.generation(),
        })
    }
}