xenith-core 0.1.0

Transport-agnostic traits, types, and errors for xenith cross-chain state sync
Documentation
use crate::{ChainId, Result, StateKey, StateValue, XenithError};
use async_trait::async_trait;

/// Resolves a set of conflicting [`StateValue`]s into a single authoritative value.
///
/// Implementations must be `Send + Sync`. The caller is always responsible for
/// choosing a resolver — xenith never resolves `Diverged` state automatically.
///
/// # Example
///
/// ```rust,no_run
/// use xenith_core::{ConflictResolver, LatestVersionResolver, StateKey};
/// use std::sync::Arc;
///
/// # async fn example(candidates: Vec<(xenith_core::ChainId, xenith_core::StateValue)>) {
/// let resolver = LatestVersionResolver;
/// let key = StateKey::new("proto", "pool", "0xabc");
/// let resolved = resolver.resolve(&key, candidates).await.unwrap();
/// # }
/// ```
#[async_trait]
pub trait ConflictResolver: Send + Sync {
    /// Reduce `candidates` (one value per chain) to a single resolved [`StateValue`].
    async fn resolve(
        &self,
        key: &StateKey,
        candidates: Vec<(ChainId, StateValue)>,
    ) -> Result<StateValue>;
}

/// Picks the candidate with the highest `version` field.
///
/// If two candidates share the same version the one with the lower chain ID wins,
/// giving a deterministic tie-break.
pub struct LatestVersionResolver;

#[async_trait]
impl ConflictResolver for LatestVersionResolver {
    async fn resolve(
        &self,
        key: &StateKey,
        candidates: Vec<(ChainId, StateValue)>,
    ) -> Result<StateValue> {
        candidates
            .into_iter()
            .map(|(_, v)| v)
            .max_by_key(|v| v.version)
            .ok_or_else(|| XenithError::StoreError(format!("no candidates for key {key}")))
    }
}

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

    fn val(chain: u64, ts: u64) -> (ChainId, StateValue) {
        use crate::StateVersion;
        (
            ChainId(chain),
            StateValue {
                data: Bytes::from_static(b"d"),
                version: StateVersion {
                    timestamp_ms: ts,
                    sequence: 0,
                    source_chain: chain,
                },
                updated_at: 0,
                source_chain: ChainId(chain),
            },
        )
    }

    #[tokio::test]
    async fn picks_highest_version() {
        use crate::StateVersion;
        let key = StateKey::new("p", "e", "1");
        let result = LatestVersionResolver
            .resolve(&key, vec![val(1, 3), val(42161, 7), val(10, 2)])
            .await
            .unwrap();
        assert_eq!(
            result.version,
            StateVersion {
                timestamp_ms: 7,
                sequence: 0,
                source_chain: 42161
            }
        );
    }

    #[tokio::test]
    async fn empty_candidates_returns_error() {
        let key = StateKey::new("p", "e", "1");
        assert!(LatestVersionResolver.resolve(&key, vec![]).await.is_err());
    }
}