hashgraph-like-consensus 0.1.2

A lightweight Rust library for making binary decisions in networks using hashgraph-style consensus
Documentation
//! Core request and event types.
//!
//! [`CreateProposalRequest`] is the input for creating new proposals.
//! [`ConsensusEvent`] represents outcomes emitted via the event bus.

use std::time::Duration;

use crate::{
    error::ConsensusError,
    protos::consensus::v1::Proposal,
    utils::{current_timestamp, generate_id, validate_expected_voters_count, validate_timeout},
};

/// Events emitted by the consensus service when a proposal reaches a terminal state.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConsensusEvent {
    /// Consensus was reached! The proposal has a final result (yes or no).
    ConsensusReached {
        proposal_id: u32,
        result: bool,
        timestamp: u64,
    },
    /// Consensus failed - not enough votes were collected before the timeout.
    ConsensusFailed { proposal_id: u32, timestamp: u64 },
}

/// Internal transition result returned after adding a vote to a session.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionTransition {
    /// Session remains active with no outcome yet.
    StillActive,
    /// Session converged to a boolean result.
    ConsensusReached(bool),
}

/// Parameters for creating a new proposal.
///
/// All fields are validated on construction via [`CreateProposalRequest::new`].
/// The `expiration_timestamp` is a relative duration in seconds that gets converted
/// to an absolute timestamp when the proposal is created.
#[derive(Debug, Clone)]
pub struct CreateProposalRequest {
    /// A short name for the proposal (e.g., "Upgrade to v2").
    pub name: String,
    /// Additional details about what's being voted on.
    pub payload: Vec<u8>,
    /// The address (public key bytes) of whoever created this proposal.
    pub proposal_owner: Vec<u8>,
    /// How many people are expected to vote (used to calculate consensus threshold).
    pub expected_voters_count: u32,
    /// The timestamp at which the proposal becomes outdated.
    pub expiration_timestamp: u64,
    /// What happens if votes are tied: `true` means YES wins, `false` means NO wins.
    pub liveness_criteria_yes: bool,
}

impl CreateProposalRequest {
    /// Create a new proposal request with validation.
    pub fn new(
        name: String,
        payload: Vec<u8>,
        proposal_owner: Vec<u8>,
        expected_voters_count: u32,
        expiration_timestamp: u64,
        liveness_criteria_yes: bool,
    ) -> Result<Self, ConsensusError> {
        validate_expected_voters_count(expected_voters_count)?;
        validate_timeout(Duration::from_secs(expiration_timestamp))?;
        let request = Self {
            name,
            payload,
            proposal_owner,
            expected_voters_count,
            expiration_timestamp,
            liveness_criteria_yes,
        };
        Ok(request)
    }

    /// Convert this request into an actual proposal.
    ///
    /// Generates a unique proposal ID and sets the creation timestamp. The proposal
    /// starts with round 1 and no votes.
    pub fn into_proposal(self) -> Result<Proposal, ConsensusError> {
        let proposal_id = generate_id();
        let now = current_timestamp()?;

        Ok(Proposal {
            name: self.name,
            payload: self.payload,
            proposal_id,
            proposal_owner: self.proposal_owner,
            votes: vec![],
            expected_voters_count: self.expected_voters_count,
            round: 1,
            timestamp: now,
            expiration_timestamp: now.saturating_add(self.expiration_timestamp),
            liveness_criteria_yes: self.liveness_criteria_yes,
        })
    }
}

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

    #[test]
    fn into_proposal_should_not_overflow_expiration_timestamp() {
        let request = CreateProposalRequest::new(
            "overflow-check".to_string(),
            vec![],
            vec![1u8; 20],
            1,
            u64::MAX,
            true,
        )
        .expect("request should be valid");

        // Desired behavior: proposal creation should not panic on overflow-prone input,
        // and expiration should never be earlier than creation timestamp.
        let proposal = request
            .into_proposal()
            .expect("proposal creation should handle large expiration safely");

        assert!(
            proposal.expiration_timestamp >= proposal.timestamp,
            "expiration must not overflow below creation timestamp"
        );
    }
}