hop-relay 2.0.0

Hop-Corr relay (batch apply + optional HTTP endpoint)
Documentation
#![forbid(unsafe_code)]
//! Relay-side validation and ordered bundle application.

use core::fmt;

use hop_actions::Action;
use hop_channels::{Channel, ChannelError};

#[cfg(feature = "net")]
pub mod net;

/// Successful bundle application metadata.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BundleReceipt {
    /// Channel height before the first action.
    pub start_height: u64,
    /// Channel height after the final action.
    pub end_height: u64,
    /// Number of committed actions.
    pub committed: usize,
}

/// A bundle failure with an explicit committed-prefix count.
#[derive(Debug)]
pub struct BundleError {
    /// Zero-based action index that failed.
    pub action_index: usize,
    /// Number of actions committed before the failure.
    pub committed: usize,
    /// Underlying channel failure.
    pub source: ChannelError,
}

impl fmt::Display for BundleError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "bundle action {} failed after {} commits: {}",
            self.action_index, self.committed, self.source
        )
    }
}

impl std::error::Error for BundleError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&self.source)
    }
}

/// Validates a complete bundle, then applies it in the supplied order.
///
/// Polynomial actions over one channel commute, so no speculative scheduler is
/// needed. Validation is completed before the first mutation. Storage failures
/// can still produce a committed prefix, which is reported by [`BundleError`].
pub fn apply_bundle(
    channel: &mut Channel,
    actions: &[Action],
) -> Result<BundleReceipt, BundleError> {
    for (index, action) in actions.iter().enumerate() {
        if let Err(source) = action
            .validate_for_dimension(channel.dimension())
            .map_err(ChannelError::from)
        {
            return Err(BundleError {
                action_index: index,
                committed: 0,
                source,
            });
        }
    }

    let start_height = channel.height();
    for (index, action) in actions.iter().enumerate() {
        if let Err(source) = channel.apply(action) {
            return Err(BundleError {
                action_index: index,
                committed: index,
                source,
            });
        }
    }
    Ok(BundleReceipt {
        start_height,
        end_height: channel.height(),
        committed: actions.len(),
    })
}