minerva 0.2.0

Causal ordering for distributed systems
//! Crash counterexamples that make fleet durability duties load-bearing.

extern crate alloc;

use alloc::vec::Vec;
use core::num::NonZeroUsize;

use crate::metis::Anchor;
use crate::metis::tests::support::dot as d;

use super::super::fabric::Fabric;
use super::super::replica::Replica;
use super::super::{Note, act, assert_converged, fleet_of, resync};
use crate::metis::dot::RawDot;

/// The mint duty's falsifier: a dotted delta must be durable before its
/// note posts, because the dot allocator re-derives from the journal at
/// restart. A consumer that broadcasts first and fences later can crash
/// in between; the restarted allocator sits below a dot the fleet
/// already holds, the next mint reuses it for different content, and one
/// identity names two payloads forever. Content follows the dot (the
/// survivor law keeps each store's first spelling), so cross-delivery
/// cannot cure the fork: this is the one crash hazard that is corruption
/// rather than lag, which is why the recipe fences the mint and not the
/// receipt.
#[test]
fn an_unfenced_mint_reused_after_restart_forks_identity() {
    const ROSTER: [u32; 3] = [1, 2, 3];
    let depth = NonZeroUsize::new(2).expect("positive");
    let mut two = Replica::new(2, &ROSTER, depth);
    let mut three = Replica::new(3, &ROSTER, depth);

    // Station 1 weaves twice. The first weave floods; the second reaches
    // station 2 only (the fabric is slow toward station 3).
    let mut one = Replica::new(1, &ROSTER, depth);
    let mut first_out = Vec::new();
    let _ = one.insert_visible(0, &mut first_out);
    let mut second_out = Vec::new();
    let emitted = one.insert_visible(1, &mut second_out);
    assert_eq!(emitted, d(1, 2), "the second mint");
    for note in &first_out {
        two.handle(note, &mut Vec::new());
        three.handle(note, &mut Vec::new());
    }
    for note in &second_out {
        two.handle(note, &mut Vec::new());
    }

    // The crash, with the duty violated: the journal a lying fence leaves
    // behind lacks a note the pre-crash replica already spoke. Honest
    // emission paths cannot produce this journal (the mint is journaled
    // in the same stroke that emits it); the tamper models the consumer
    // that emitted first.
    let mut journal = one.journal().clone();
    journal
        .notes
        .retain(|note| !matches!(note, Note::Old { dots, .. } if dots.contains(&d(1, 2))));
    let mut one = Replica::rehydrate(1, &ROSTER, depth, &journal);

    // The re-derived allocator sits below the emitted dot, so the next
    // mint reuses it, here with a different anchor (a head insert, so
    // the sided rule spells `Before` where the original spelled `After`).
    let mut remint_out = Vec::new();
    let reminted = one.insert_visible(0, &mut remint_out);
    assert_eq!(
        reminted,
        d(1, 2),
        "the forgotten allocator re-mints the emitted dot"
    );
    for note in &remint_out {
        three.handle(note, &mut Vec::new());
    }

    // One identity, two payloads: the fleet already disagrees.
    let at_two = two
        .text()
        .store()
        .locus(d(1, 2))
        .expect("station 2 holds the original spelling")
        .anchor;
    let at_three = three
        .text()
        .store()
        .locus(d(1, 2))
        .expect("station 3 holds the re-minted spelling")
        .anchor;
    assert_eq!(
        at_two,
        Anchor::After(RawDot {
            station: 1,
            counter: 1
        })
    );
    assert_eq!(
        at_three,
        Anchor::Before(RawDot {
            station: 1,
            counter: 1
        })
    );

    // And cross-delivery cannot cure it: content follows the dot, so
    // each store keeps its first spelling and byte identity is gone for
    // good.
    for note in &second_out {
        three.handle(note, &mut Vec::new());
    }
    for note in &remint_out {
        two.handle(note, &mut Vec::new());
    }
    assert_eq!(
        two.text()
            .store()
            .locus(d(1, 2))
            .expect("still held")
            .anchor,
        Anchor::After(RawDot {
            station: 1,
            counter: 1
        }),
        "the survivor law keeps station 2's first spelling"
    );
    assert_eq!(
        three
            .text()
            .store()
            .locus(d(1, 2))
            .expect("still held")
            .anchor,
        Anchor::Before(RawDot {
            station: 1,
            counter: 1
        }),
        "the survivor law keeps station 3's first spelling"
    );
    assert_ne!(
        two.text().store().to_bytes(),
        three.text().store().to_bytes(),
        "one dot, two payloads: the fleet can never read one byte sequence again"
    );
}

/// The oath duty's falsifier: a retirement acknowledgement is testimony,
/// so everything it binds (the applied removals it names) must be
/// durable before it leaves. A consumer that acknowledges first and
/// fences later can crash in between; the restarted writer has forgotten
/// both the removal and its own published oath, the element reads
/// visible again, and the disciplined weave anchors the very dot the
/// oath licensed peers to excise. The weave strands at every excised
/// peer, the exact divergence the writer discipline exists to prevent
/// (ruling R-42), reopened by amnesia alone.
#[test]
fn an_unfenced_oath_lets_a_restarted_writer_strand_its_weave() {
    const ROSTER: [u32; 3] = [1, 2, 3];
    let depth = NonZeroUsize::new(2).expect("positive");
    let mut fleet = fleet_of(&ROSTER, depth);
    let mut fabric = Fabric::new(0x0A7B_0A7B, &ROSTER, 0);

    // One element, delivered everywhere; station 3 deletes it; the
    // acknowledgement round gathers under the cut gate at every member.
    let woven = act(&mut fabric, &mut fleet, 1, |replica, out| {
        replica.insert_visible(0, out)
    });
    fabric.drain(&mut fleet);
    let deleted = act(&mut fabric, &mut fleet, 3, |replica, out| {
        replica.delete_visible(0, out)
    });
    assert_eq!(deleted, Some(woven), "station 3 deletes the element");
    fabric.drain(&mut fleet);

    // Stations 1 and 3 excise on the gathered meet: local hygiene the
    // fleet-wide oath round licenses.
    for station in [1, 3] {
        assert_eq!(
            fleet.get_mut(&station).expect("roster member").condense(),
            1,
            "the gathered meet licenses the excision"
        );
    }

    // Station 2 crashes with the oath duty violated: its acknowledgement
    // was already counted everywhere, but the restart-side journal lacks
    // the removal the oath was about (station 3's one mint).
    let mut journal = fleet[&2].journal().clone();
    journal.notes.retain(
        |note| !matches!(note, Note::Old { dots, .. } if dots.iter().any(|dot| dot.station() == 3)),
    );
    let _ = fleet.insert(2, Replica::rehydrate(2, &ROSTER, depth, &journal));

    // The zombie: the element reads visible again at the amnesiac and its
    // published oath rebuilt empty, so the disciplined weave anchors the
    // excised dot in good conscience.
    let stranded = act(&mut fabric, &mut fleet, 2, |replica, out| {
        replica.insert_disciplined(1, out)
    });
    fabric.drain(&mut fleet);

    // Placed at the amnesiac, present but unreachable forever at the
    // excised peers.
    assert!(
        fleet[&2].text().store().is_reachable(stranded),
        "the amnesiac reads its own weave in place"
    );
    for station in [1, 3] {
        let replica = &fleet[&station];
        let store = replica.text().store();
        assert!(
            store.locus(stranded).is_some(),
            "the weave was delivered to station {station}"
        );
        assert!(
            !store.is_reachable(stranded),
            "the weave strands at excised station {station}: the forgotten \
             oath re-anchored a dot the fleet had lawfully excised"
        );
    }
}

/// The durable-floor duty's falsifier, and the shape of its failure: a
/// report whose floor outran the fence regresses invisibly at restart
/// (every peer's tracker folds by join, so the lower re-report is
/// absorbed, exactly the masking PRD 0011 documents), and the
/// declaration licensed on the masked claim opens a window the amnesiac
/// cannot rebuild. Because every irreversible step in this protocol is
/// gated on a roster-wide meet, the failure is a wedge, not corruption:
/// nothing seals while the window waits on the amnesiac, and the repair
/// lane (the consumer's anti-entropy) restores liveness. This is the
/// honest price of masking; the corruption cases live in the two
/// falsifiers above, which is why the recipe's hard duties are the mint
/// and the oath while receipt durability stays a batching policy.
#[test]
fn a_report_beyond_the_fenced_floor_wedges_the_window_until_repair() {
    const ROSTER: [u32; 3] = [1, 2, 3];
    let depth = NonZeroUsize::new(2).expect("positive");
    let mut fleet = fleet_of(&ROSTER, depth);
    let mut fabric = Fabric::new(0x00D5_F100, &ROSTER, 0);

    // Two weaves from station 1 flood; every report claims both.
    let _ = act(&mut fabric, &mut fleet, 1, |replica, out| {
        replica.insert_visible(0, out)
    });
    let _ = act(&mut fabric, &mut fleet, 1, |replica, out| {
        replica.insert_visible(1, out)
    });
    fabric.drain(&mut fleet);

    // Station 2 crashes behind a lying fence: the journal lost the tail
    // delta its last report had already claimed.
    let mut journal = fleet[&2].journal().clone();
    journal
        .notes
        .retain(|note| !matches!(note, Note::Old { dots, .. } if dots.contains(&d(1, 2))));
    let _ = fleet.insert(2, Replica::rehydrate(2, &ROSTER, depth, &journal));

    // The regression is masked (the join absorbs the lower re-report),
    // so the declaration license still reads clean at station 1.
    let _ = act(&mut fabric, &mut fleet, 1, |replica, out| {
        replica.try_declare(out)
    })
    .expect("the masked watermark licenses the declaration");
    fabric.drain(&mut fleet);

    // The wedge: the amnesiac's gap-free floor cannot cover the
    // declaration, so it never confirms, no winner fixes, nobody adopts,
    // and nothing seals. Fail-closed is the meet gating's gift: peers
    // reclaimed nothing below the masked claim, so nothing is lost.
    for replica in fleet.values() {
        assert_eq!(
            replica.generation(),
            1,
            "no seal may fire while the window waits on the amnesiac"
        );
        assert!(
            !replica.adopted(),
            "no adoption without the amnesiac's confirmation"
        );
    }

    // The cure is the repair lane, not the protocol: peers re-serve the
    // lost delta, the floor genuinely rises, and the lifecycle completes.
    resync(&mut fabric, &fleet, 2);
    fabric.drain(&mut fleet);
    for replica in fleet.values() {
        assert_eq!(replica.generation(), 2, "the repaired window seals");
        assert_eq!(replica.seals.len(), 1);
    }
    assert_converged(&fleet);
}