lodestone-types 0.0.16

Shared semantic types for Lodestone components and public Rust APIs
Documentation
//! Canonical interpretation of one exact mutation across authority layers.
//!
//! This is a transport-neutral derived model, not a serialized receipt or a
//! second completion authority. Each durable owner retains its existing
//! receipt/state and maps an exact read-back observation into this vocabulary.

use std::error::Error;
use std::fmt::{self, Display, Formatter};

/// Identity that must remain byte-exact from prepare through visibility.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct MutationReplayIdentity {
    authority_binding: [u8; 32],
    operation_identity: [u8; 32],
    semantic_digest: [u8; 32],
}

impl MutationReplayIdentity {
    pub fn new(
        authority_binding: [u8; 32],
        operation_identity: [u8; 32],
        semantic_digest: [u8; 32],
    ) -> Result<Self, MutationLifecycleError> {
        if authority_binding == [0; 32] {
            return Err(MutationLifecycleError::ZeroAuthorityBinding);
        }
        if operation_identity == [0; 32] {
            return Err(MutationLifecycleError::ZeroOperationIdentity);
        }
        if semantic_digest == [0; 32] {
            return Err(MutationLifecycleError::ZeroSemanticDigest);
        }
        Ok(Self {
            authority_binding,
            operation_identity,
            semantic_digest,
        })
    }

    #[must_use]
    pub const fn authority_binding(self) -> [u8; 32] {
        self.authority_binding
    }

    #[must_use]
    pub const fn operation_identity(self) -> [u8; 32] {
        self.operation_identity
    }

    #[must_use]
    pub const fn semantic_digest(self) -> [u8; 32] {
        self.semantic_digest
    }
}

/// The four monotonic lifecycle stages shared by mutation authorities.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum MutationLifecycleStage {
    NotSubmitted,
    Prepared,
    Committed,
    Visible,
}

/// Exact meaning of a prepared observation.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PreparedMutationDisposition {
    /// The durable prepare proves that replaying the same identity is safe.
    SafelyReplayable,
    /// Submission is in progress; callers may observe but must not resubmit.
    StillPending,
}

/// Closed result of resolving a possibly submitted mutation.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SubmittedIndeterminateResolution {
    Committed { visible: bool },
    SafelyReplayable,
    StillPending,
    ProvenNotSubmitted,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MutationLifecycleState {
    NotSubmitted,
    Prepared(PreparedMutationDisposition),
    Committed,
    Visible,
}

/// One exact authority observation in the canonical lifecycle.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MutationLifecycleObservation {
    replay_identity: MutationReplayIdentity,
    state: MutationLifecycleState,
}

impl MutationLifecycleObservation {
    #[must_use]
    pub const fn proven_not_submitted(replay_identity: MutationReplayIdentity) -> Self {
        Self {
            replay_identity,
            state: MutationLifecycleState::NotSubmitted,
        }
    }

    #[must_use]
    pub const fn prepared(replay_identity: MutationReplayIdentity, disposition: PreparedMutationDisposition) -> Self {
        Self {
            replay_identity,
            state: MutationLifecycleState::Prepared(disposition),
        }
    }

    #[must_use]
    pub const fn committed(replay_identity: MutationReplayIdentity) -> Self {
        Self {
            replay_identity,
            state: MutationLifecycleState::Committed,
        }
    }

    #[must_use]
    pub const fn visible(replay_identity: MutationReplayIdentity) -> Self {
        Self {
            replay_identity,
            state: MutationLifecycleState::Visible,
        }
    }

    #[must_use]
    pub const fn replay_identity(self) -> MutationReplayIdentity {
        self.replay_identity
    }

    #[must_use]
    pub const fn stage(self) -> MutationLifecycleStage {
        match self.state {
            MutationLifecycleState::NotSubmitted => MutationLifecycleStage::NotSubmitted,
            MutationLifecycleState::Prepared(_) => MutationLifecycleStage::Prepared,
            MutationLifecycleState::Committed => MutationLifecycleStage::Committed,
            MutationLifecycleState::Visible => MutationLifecycleStage::Visible,
        }
    }

    #[must_use]
    pub const fn prepared_disposition(self) -> Option<PreparedMutationDisposition> {
        match self.state {
            MutationLifecycleState::Prepared(disposition) => Some(disposition),
            MutationLifecycleState::NotSubmitted
            | MutationLifecycleState::Committed
            | MutationLifecycleState::Visible => None,
        }
    }

    #[must_use]
    pub const fn resolve_submitted_indeterminate(self) -> SubmittedIndeterminateResolution {
        match self.state {
            MutationLifecycleState::NotSubmitted => SubmittedIndeterminateResolution::ProvenNotSubmitted,
            MutationLifecycleState::Prepared(PreparedMutationDisposition::SafelyReplayable) => {
                SubmittedIndeterminateResolution::SafelyReplayable
            }
            MutationLifecycleState::Prepared(PreparedMutationDisposition::StillPending) => {
                SubmittedIndeterminateResolution::StillPending
            }
            MutationLifecycleState::Committed => SubmittedIndeterminateResolution::Committed { visible: false },
            MutationLifecycleState::Visible => SubmittedIndeterminateResolution::Committed { visible: true },
        }
    }

    pub fn require_monotonic_successor(self, successor: Self) -> Result<(), MutationLifecycleError> {
        if self.replay_identity != successor.replay_identity {
            return Err(MutationLifecycleError::ReplayIdentityChanged);
        }
        let stage = self.stage();
        let successor_stage = successor.stage();
        if successor_stage < stage {
            return Err(MutationLifecycleError::StageRegressed {
                from: stage,
                to: successor_stage,
            });
        }
        Ok(())
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MutationLifecycleError {
    ZeroAuthorityBinding,
    ZeroOperationIdentity,
    ZeroSemanticDigest,
    ReplayIdentityChanged,
    StageRegressed {
        from: MutationLifecycleStage,
        to: MutationLifecycleStage,
    },
    OutcomeEvidenceRetired,
}

impl Display for MutationLifecycleError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        match self {
            Self::ZeroAuthorityBinding => formatter.write_str("mutation authority binding cannot be zero"),
            Self::ZeroOperationIdentity => formatter.write_str("mutation operation identity cannot be zero"),
            Self::ZeroSemanticDigest => formatter.write_str("mutation semantic digest cannot be zero"),
            Self::ReplayIdentityChanged => {
                formatter.write_str("mutation replay identity changed across lifecycle observations")
            }
            Self::StageRegressed { from, to } => {
                write!(formatter, "mutation lifecycle regressed from {from:?} to {to:?}")
            }
            Self::OutcomeEvidenceRetired => {
                formatter.write_str("mutation outcome evidence retired before deterministic resolution")
            }
        }
    }
}

impl Error for MutationLifecycleError {}

#[cfg(test)]
mod tests {
    use super::*;

    fn replay_identity(value: u8) -> Result<MutationReplayIdentity, MutationLifecycleError> {
        MutationReplayIdentity::new(
            [value; 32],
            [value.saturating_add(1); 32],
            [value.saturating_add(2); 32],
        )
    }

    #[test]
    fn submitted_indeterminate_has_exactly_four_resolution_classes() -> Result<(), Box<dyn Error>> {
        let identity = replay_identity(1)?;
        assert_eq!(
            MutationLifecycleObservation::proven_not_submitted(identity).resolve_submitted_indeterminate(),
            SubmittedIndeterminateResolution::ProvenNotSubmitted
        );
        assert_eq!(
            MutationLifecycleObservation::prepared(identity, PreparedMutationDisposition::SafelyReplayable)
                .resolve_submitted_indeterminate(),
            SubmittedIndeterminateResolution::SafelyReplayable
        );
        assert_eq!(
            MutationLifecycleObservation::prepared(identity, PreparedMutationDisposition::StillPending)
                .resolve_submitted_indeterminate(),
            SubmittedIndeterminateResolution::StillPending
        );
        assert_eq!(
            MutationLifecycleObservation::committed(identity).resolve_submitted_indeterminate(),
            SubmittedIndeterminateResolution::Committed { visible: false }
        );
        assert_eq!(
            MutationLifecycleObservation::visible(identity).resolve_submitted_indeterminate(),
            SubmittedIndeterminateResolution::Committed { visible: true }
        );
        Ok(())
    }

    #[test]
    fn lifecycle_is_monotonic_and_identity_exact() -> Result<(), Box<dyn Error>> {
        let identity = replay_identity(7)?;
        let not_submitted = MutationLifecycleObservation::proven_not_submitted(identity);
        let replayable =
            MutationLifecycleObservation::prepared(identity, PreparedMutationDisposition::SafelyReplayable);
        let pending = MutationLifecycleObservation::prepared(identity, PreparedMutationDisposition::StillPending);
        let committed = MutationLifecycleObservation::committed(identity);
        let visible = MutationLifecycleObservation::visible(identity);
        not_submitted.require_monotonic_successor(replayable)?;
        replayable.require_monotonic_successor(pending)?;
        pending.require_monotonic_successor(replayable)?;
        pending.require_monotonic_successor(committed)?;
        committed.require_monotonic_successor(visible)?;
        assert!(matches!(
            visible.require_monotonic_successor(committed),
            Err(MutationLifecycleError::StageRegressed { .. })
        ));
        assert_eq!(
            committed.require_monotonic_successor(MutationLifecycleObservation::visible(replay_identity(8)?,)),
            Err(MutationLifecycleError::ReplayIdentityChanged)
        );
        Ok(())
    }

    #[test]
    fn replay_identity_refuses_ambiguous_components() {
        assert_eq!(
            MutationReplayIdentity::new([0; 32], [1; 32], [2; 32]),
            Err(MutationLifecycleError::ZeroAuthorityBinding)
        );
        assert_eq!(
            MutationReplayIdentity::new([1; 32], [0; 32], [2; 32]),
            Err(MutationLifecycleError::ZeroOperationIdentity)
        );
        assert_eq!(
            MutationReplayIdentity::new([1; 32], [2; 32], [0; 32]),
            Err(MutationLifecycleError::ZeroSemanticDigest)
        );
    }
}