liminal-protocol 0.14.0

Shared participant-lifecycle protocol types for liminal
Documentation
use alloc::vec;
use alloc::vec::Vec;

use super::{
    CapsuleDecodeError, CapsuleRestoreError, ConversationDecision, ConversationGenesis,
    LifecycleCapsule, LifecycleView, ParticipantConversation,
};

const CONVERSATION_ID: u64 = 91;

/// The shell after its genesis validation committed, through the live path.
fn validated() -> Result<ParticipantConversation, &'static str> {
    let fresh = ParticipantConversation::from_genesis(ConversationGenesis::new(CONVERSATION_ID));
    let ConversationDecision::Commit(commit) = fresh.decide_genesis_validation() else {
        return Err("a fresh genesis must select its one-shot event");
    };
    Ok(commit.commit())
}

/// Captures a shell with no lifecycle yet: the pre-enrollment capsule, whose
/// bytes are exactly the committed v1.0 conversation-only envelope.
fn capsule_of(
    conversation: &ParticipantConversation,
    covered: u64,
) -> Result<LifecycleCapsule, &'static str> {
    LifecycleCapsule::capture(&LifecycleView {
        sealed: None,
        conversation,
        lifecycle: None,
        covered_log_sequence: covered,
    })
    .map_err(|_| "a shell without a lifecycle always captures")
}

/// Capture, encode, decode and restore, answering the restored shell.
fn round_trip(
    conversation: &ParticipantConversation,
    covered: u64,
) -> Result<ParticipantConversation, &'static str> {
    let bytes = capsule_of(conversation, covered)?.encode_canonical();
    let decoded = LifecycleCapsule::decode_canonical(&bytes).map_err(|_| "decode refused")?;
    assert_eq!(decoded, capsule_of(conversation, covered)?);
    let restored = decoded.restore().map_err(|_| "restore refused")?;
    assert_eq!(restored.covered_log_sequence(), covered);
    Ok(restored.into_parts().conversation)
}

#[test]
fn a_fresh_shell_round_trips() -> Result<(), &'static str> {
    let fresh = ParticipantConversation::from_genesis(ConversationGenesis::new(CONVERSATION_ID));
    assert_eq!(round_trip(&fresh, 0)?, fresh);
    Ok(())
}

#[test]
fn a_validated_shell_round_trips() -> Result<(), &'static str> {
    let shell = validated()?;
    assert_eq!(round_trip(&shell, 4)?, shell);
    Ok(())
}

#[test]
fn a_shell_deep_in_its_log_round_trips() -> Result<(), &'static str> {
    let shell = ParticipantConversation::from_test_state(
        ConversationGenesis::new(CONVERSATION_ID),
        u64::MAX - 1,
        true,
    );
    assert_eq!(round_trip(&shell, u64::MAX)?, shell);
    Ok(())
}

#[test]
fn the_canonical_bytes_are_stable() -> Result<(), &'static str> {
    let bytes = capsule_of(&validated()?, 4)?.encode_canonical();
    assert_eq!(
        bytes,
        vec![
            b'L', b'P', b'C', b'K', 0, 1, 0, 0, // magic, v1.0
            0, 0, 0, 0, 0, 0, 0, 91, // conversation
            0, 0, 0, 0, 0, 0, 0, 4, // covered log sequence
            0, 0, 0, 15, // body length
            0, 1, 0, 0, 0, 9, // conversation section, 9 bytes
            0, 0, 0, 0, 0, 0, 0, 1, // next event ordinal
            1, // genesis validated
        ]
    );
    Ok(())
}

fn canonical() -> Result<Vec<u8>, &'static str> {
    Ok(capsule_of(&validated()?, 4)?.encode_canonical())
}

fn decode(bytes: &[u8]) -> Option<CapsuleDecodeError> {
    LifecycleCapsule::decode_canonical(bytes).err()
}

#[test]
fn every_malformed_capsule_is_refused_by_name() -> Result<(), &'static str> {
    let good = canonical()?;

    assert_eq!(
        decode(good.get(..27).ok_or("short")?),
        Some(CapsuleDecodeError::Truncated {
            required: 28,
            available: 27
        })
    );

    let mut magic = good.clone();
    *magic.first_mut().ok_or("empty")? = b'X';
    assert_eq!(decode(&magic), Some(CapsuleDecodeError::InvalidMagic));

    let mut version = good.clone();
    *version.get_mut(5).ok_or("short")? = 2;
    assert_eq!(
        decode(&version),
        Some(CapsuleDecodeError::UnsupportedCodec { major: 2, minor: 0 })
    );

    let mut long = good.clone();
    long.push(0);
    assert_eq!(
        decode(&long),
        Some(CapsuleDecodeError::NonCanonicalLength {
            declared_body_len: 15,
            actual_body_len: 16
        })
    );

    let mut trailing = long;
    *trailing.get_mut(27).ok_or("short")? = 16;
    assert_eq!(
        decode(&trailing),
        Some(CapsuleDecodeError::TrailingBytes { trailing: 1 })
    );

    let mut unknown = good.clone();
    *unknown.get_mut(29).ok_or("short")? = 9;
    assert_eq!(
        decode(&unknown),
        Some(CapsuleDecodeError::UnknownSection { tag: 9 })
    );

    let mut empty_body = good.get(..28).ok_or("short")?.to_vec();
    *empty_body.get_mut(27).ok_or("short")? = 0;
    assert_eq!(
        decode(&empty_body),
        Some(CapsuleDecodeError::MissingSection { tag: 1 })
    );

    let mut flags = good;
    *flags.last_mut().ok_or("empty")? = 0b11;
    assert_eq!(
        decode(&flags),
        Some(CapsuleDecodeError::NonCanonicalSection { tag: 1 })
    );
    Ok(())
}

#[test]
fn a_section_of_the_wrong_length_is_refused() -> Result<(), &'static str> {
    let mut short = canonical()?;
    short.pop();
    *short.get_mut(27).ok_or("short")? = 14;
    *short.get_mut(33).ok_or("short")? = 8;
    assert_eq!(
        decode(&short),
        Some(CapsuleDecodeError::NonCanonicalSectionLength {
            tag: 1,
            declared: 8
        })
    );
    Ok(())
}

/// Genesis validation is the first event, so a shell is validated exactly
/// when it is past ordinal 0; a capsule saying otherwise is refused.
#[test]
fn a_shell_whose_ordinal_and_validation_disagree_is_refused() -> Result<(), &'static str> {
    let mut unvalidated_past_genesis = canonical()?;
    *unvalidated_past_genesis.last_mut().ok_or("empty")? = 0;
    assert_eq!(
        decode(&unvalidated_past_genesis),
        Some(CapsuleDecodeError::NonCanonicalSection { tag: 1 })
    );

    let mut validated_at_zero = canonical()?;
    *validated_at_zero.get_mut(41).ok_or("short")? = 0;
    assert_eq!(
        decode(&validated_at_zero),
        Some(CapsuleDecodeError::NonCanonicalSection { tag: 1 })
    );

    let inconsistent = ParticipantConversation::from_test_state(
        ConversationGenesis::new(CONVERSATION_ID),
        3,
        false,
    );
    assert_eq!(
        capsule_of(&inconsistent, 3)?.restore().err(),
        Some(CapsuleRestoreError::InconsistentConversation)
    );
    Ok(())
}