minerva 0.2.0

Causal ordering for distributed systems
//! Typed lifecycle-frame refusals and bounded decode preflights.

use super::*;

/// Every frame refuses an unknown version byte with its own typed
/// error, before any other work.
#[test]
fn test_unknown_versions_refuse() {
    let (_, sealed) = displaced_window();
    let confirmation = ConfirmationRecord::new(address(1, (2, 3)), cut(&[(1, 1)]));
    let adoption = AdoptionReportRecord::new(address(1, (2, 3)), 5);
    let seal = SealRecord::from_sealed(&sealed);
    let proof = LineageProofRecord::try_new(Vec::new()).unwrap();
    let declaration = declaration(&[1, 2], d(2, 3), 7, &[(1, 2), (2, 2)]);

    let mut frame = declaration.to_bytes();
    frame[0] = 0x7F;
    assert_eq!(
        Declaration::from_bytes(&frame),
        Err(DeclarationDecodeError::UnknownVersion(0x7F))
    );
    let mut frame = confirmation.to_bytes();
    frame[0] = 0x7F;
    assert_eq!(
        ConfirmationRecord::from_bytes(&frame),
        Err(ConfirmationDecodeError::UnknownVersion(0x7F))
    );
    let mut frame = adoption.to_bytes();
    frame[0] = 0x7F;
    assert_eq!(
        AdoptionReportRecord::from_bytes(&frame),
        Err(AdoptionReportDecodeError::UnknownVersion(0x7F))
    );
    let mut frame = seal.to_bytes();
    frame[0] = 0x7F;
    assert_eq!(
        SealRecord::from_bytes(&frame),
        Err(SealDecodeError::UnknownVersion(0x7F))
    );
    let mut frame = proof.to_bytes();
    frame[0] = 0x7F;
    assert_eq!(
        LineageProofRecord::from_bytes(&frame),
        Err(LineageProofDecodeError::UnknownVersion(0x7F))
    );
}

/// Trailing bytes refuse on every frame: `from_bytes` is the exact-frame
/// door.
#[test]
fn test_trailing_bytes_refuse() {
    let record = declaration(&[1, 2], d(2, 3), 7, &[(1, 2), (2, 2)]);
    let mut frame = record.to_bytes();
    let exact = frame.len();
    frame.push(0);
    assert_eq!(
        Declaration::from_bytes(&frame),
        Err(DeclarationDecodeError::UnexpectedLength {
            expected: exact,
            found: exact + 1
        })
    );

    let record = AdoptionReportRecord::new(address(1, (2, 3)), 5);
    let mut frame = record.to_bytes();
    let exact = frame.len();
    frame.push(0);
    assert_eq!(
        AdoptionReportRecord::from_bytes(&frame),
        Err(AdoptionReportDecodeError::UnexpectedLength {
            expected: exact,
            found: exact + 1
        })
    );
}

/// The address identity refusals ride the nominal type: a zero
/// generation and a zero declaration counter each refuse with the
/// evidence [`EpochAddress::try_from_parts`] states once.
#[test]
fn test_zero_address_parts_refuse() {
    let record = AdoptionReportRecord::new(address(1, (2, 3)), 5);
    let mut frame = record.to_bytes();
    // Zero the generation.
    for byte in &mut frame[1..9] {
        *byte = 0;
    }
    assert_eq!(
        AdoptionReportRecord::from_bytes(&frame),
        Err(AdoptionReportDecodeError::Address(
            InvalidEpochAddress::ZeroGeneration
        ))
    );
    let mut frame = record.to_bytes();
    // Zero the declaration counter.
    for byte in &mut frame[13..21] {
        *byte = 0;
    }
    assert_eq!(
        AdoptionReportRecord::from_bytes(&frame),
        Err(AdoptionReportDecodeError::Address(
            InvalidEpochAddress::ZeroDeclarationCounter { station: 2 }
        ))
    );
}

/// A gapped cut embedding refuses with the receiver-proved gap-freedom
/// evidence: the confirmation frame is rebuilt around a have-set whose
/// station carries an exception run, and the decode names the run.
#[test]
fn test_gapped_cut_embedding_refuses() {
    let record = ConfirmationRecord::new(address(1, (2, 3)), cut(&[(1, 1)]));
    let frame = record.to_bytes();
    // Splice in a gapped have-set: dot 1 held, dot 3 held, dot 2
    // missing (floor 1, one exception run at 3).
    let mut gapped = DotSet::new();
    assert!(gapped.insert(d(1, 1)));
    assert!(gapped.insert(d(1, 3)));
    let mut spliced = frame[..21].to_vec();
    spliced.extend_from_slice(&gapped.to_bytes());
    assert_eq!(
        ConfirmationRecord::from_bytes(&spliced),
        Err(ConfirmationDecodeError::Cut(WireCutError::Gapped {
            station: 1,
            start: 3
        }))
    );
}

/// The seal's canonical-form refusals: non-ascending candidates,
/// non-ascending ledger dots, and the non-dot zero, each with its
/// evidence.
#[test]
fn test_seal_canonical_form_refusals() {
    let (_, sealed) = displaced_window();
    let record = SealRecord::from_sealed(&sealed);
    let frame = record.to_bytes();

    // Duplicate the sole candidate: equal is not ascending. The second
    // candidate row starts nowhere in this one-candidate frame, so
    // build a two-candidate frame by splicing the row twice.
    let mut doubled = frame[..25].to_vec();
    doubled[24] = 2; // candidate count 1 -> 2
    doubled.extend_from_slice(&frame[25..45]); // the candidate row
    doubled.extend_from_slice(&frame[25..45]); // again, equal
    doubled.extend_from_slice(&frame[45..]); // ledger + join unchanged
    assert_eq!(
        SealRecord::from_bytes(&doubled),
        Err(SealDecodeError::NonAscendingCandidates {
            previous: address(1, (2, 3)),
            found: address(1, (2, 3))
        })
    );

    // Swap the two ledger dots: descending refuses with both shown.
    let mut swapped = frame.clone();
    let first: [u8; 12] = frame[49..61].try_into().unwrap();
    let second: [u8; 12] = frame[61..73].try_into().unwrap();
    swapped[49..61].copy_from_slice(&second);
    swapped[61..73].copy_from_slice(&first);
    assert_eq!(
        SealRecord::from_bytes(&swapped),
        Err(SealDecodeError::NonAscendingLedger {
            previous: d(2, 3),
            found: d(1, 3)
        })
    );

    // Zero a ledger dot's index: the non-dot refuses.
    let mut zeroed = frame;
    for byte in &mut zeroed[53..61] {
        *byte = 0;
    }
    assert_eq!(
        SealRecord::from_bytes(&zeroed),
        Err(SealDecodeError::ZeroLedgerDot { station: 1 })
    );
}

/// The budget applies independently to both retained sets.
#[test]
fn test_seal_budget_refusals() {
    let (_, sealed) = displaced_window();
    let record = SealRecord::from_sealed(&sealed);
    let frame = record.to_bytes();
    assert_eq!(
        SealRecord::from_bytes_with_budget(&frame, SealDecodeBudget::new(2)),
        Ok(record)
    );
    assert_eq!(
        SealRecord::from_bytes_with_budget(&frame, SealDecodeBudget::new(0)),
        Err(SealDecodeError::TooManyCandidates {
            count: 1,
            budget: 0
        })
    );
    assert_eq!(
        SealRecord::from_bytes_with_budget(&frame, SealDecodeBudget::new(1)),
        Err(SealDecodeError::TooManyLedgerDots {
            count: 2,
            budget: 1
        })
    );
}

/// Count preflight is `O(1)` and computes target-safe diagnostics in `u64`.
#[test]
fn test_impossible_counts_refuse_in_o1() {
    let (_, sealed) = displaced_window();
    let frame = SealRecord::from_sealed(&sealed).to_bytes();
    let mut inflated = frame;
    inflated[21..25].copy_from_slice(&u32::MAX.to_be_bytes());
    // The target-uniform diagnostic: 25 bytes consumed through the
    // count field, then u32::MAX 20-byte rows plus the 4-byte ledger
    // count and 5-byte join header still owed.
    let floor = u64::from(u32::MAX) * 20 + 4 + 5;
    let expected = usize::try_from(25u64.saturating_add(floor)).unwrap_or(usize::MAX);
    assert_eq!(
        SealRecord::from_bytes(&inflated),
        Err(SealDecodeError::UnexpectedLength {
            expected,
            found: inflated.len()
        })
    );

    let proof = LineageProofRecord::try_new(Vec::new()).unwrap();
    let mut inflated = proof.to_bytes();
    inflated[1..5].copy_from_slice(&u32::MAX.to_be_bytes());
    assert!(matches!(
        LineageProofRecord::from_bytes(&inflated),
        Err(LineageProofDecodeError::UnexpectedLength { .. })
    ));
}