Skip to main content

hashgraph_like_consensus/
types.rs

1//! Core request and event types.
2//!
3//! [`CreateProposalRequest`] is the input for creating new proposals.
4//! [`ConsensusEvent`] represents outcomes emitted via the event bus.
5
6use std::time::Duration;
7
8use crate::{
9    error::ConsensusError,
10    protos::consensus::v1::Proposal,
11    utils::{current_timestamp, generate_id, validate_expected_voters_count, validate_timeout},
12};
13
14/// Events emitted by the consensus service when a proposal reaches a terminal state.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum ConsensusEvent {
17    /// Consensus was reached! The proposal has a final result (yes or no).
18    ConsensusReached {
19        proposal_id: u32,
20        result: bool,
21        timestamp: u64,
22    },
23    /// Consensus failed - not enough votes were collected before the timeout.
24    ConsensusFailed { proposal_id: u32, timestamp: u64 },
25}
26
27/// Internal transition result returned after adding a vote to a session.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum SessionTransition {
30    /// Session remains active with no outcome yet.
31    StillActive,
32    /// Session converged to a boolean result.
33    ConsensusReached(bool),
34}
35
36/// Parameters for creating a new proposal.
37///
38/// All fields are validated on construction via [`CreateProposalRequest::new`].
39/// The `expiration_timestamp` is a relative duration in seconds that gets converted
40/// to an absolute timestamp when the proposal is created.
41#[derive(Debug, Clone)]
42pub struct CreateProposalRequest {
43    /// A short name for the proposal (e.g., "Upgrade to v2").
44    pub name: String,
45    /// Additional details about what's being voted on.
46    pub payload: Vec<u8>,
47    /// The address (public key bytes) of whoever created this proposal.
48    pub proposal_owner: Vec<u8>,
49    /// How many people are expected to vote. This drives all threshold math
50    /// (`ceil(2n/3)` quorum, silent peer counting at timeout). Must match the
51    /// actual group size — a wrong value produces wrong consensus results.
52    pub expected_voters_count: u32,
53    /// The timestamp at which the proposal becomes outdated.
54    pub expiration_timestamp: u64,
55    /// How silent peers (those who never vote) are counted at timeout:
56    /// `true` = silent peers count as YES, `false` = silent peers count as NO.
57    /// Also used as the tie-breaker when all expected voters have voted and
58    /// YES/NO counts are equal.
59    pub liveness_criteria_yes: bool,
60}
61
62impl CreateProposalRequest {
63    /// Create a new proposal request with validation.
64    pub fn new(
65        name: String,
66        payload: Vec<u8>,
67        proposal_owner: Vec<u8>,
68        expected_voters_count: u32,
69        expiration_timestamp: u64,
70        liveness_criteria_yes: bool,
71    ) -> Result<Self, ConsensusError> {
72        validate_expected_voters_count(expected_voters_count)?;
73        validate_timeout(Duration::from_secs(expiration_timestamp))?;
74        let request = Self {
75            name,
76            payload,
77            proposal_owner,
78            expected_voters_count,
79            expiration_timestamp,
80            liveness_criteria_yes,
81        };
82        Ok(request)
83    }
84
85    /// Convert this request into an actual proposal.
86    ///
87    /// Generates a unique proposal ID and sets the creation timestamp. The proposal
88    /// starts with round 1 and no votes.
89    pub fn into_proposal(self) -> Result<Proposal, ConsensusError> {
90        let proposal_id = generate_id();
91        let now = current_timestamp()?;
92
93        Ok(Proposal {
94            name: self.name,
95            payload: self.payload,
96            proposal_id,
97            proposal_owner: self.proposal_owner,
98            votes: vec![],
99            expected_voters_count: self.expected_voters_count,
100            round: 1,
101            timestamp: now,
102            expiration_timestamp: now.saturating_add(self.expiration_timestamp),
103            liveness_criteria_yes: self.liveness_criteria_yes,
104        })
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::CreateProposalRequest;
111
112    #[test]
113    fn into_proposal_should_not_overflow_expiration_timestamp() {
114        let request = CreateProposalRequest::new(
115            "overflow-check".to_string(),
116            vec![],
117            vec![1u8; 20],
118            1,
119            u64::MAX,
120            true,
121        )
122        .expect("request should be valid");
123
124        // Desired behavior: proposal creation should not panic on overflow-prone input,
125        // and expiration should never be earlier than creation timestamp.
126        let proposal = request
127            .into_proposal()
128            .expect("proposal creation should handle large expiration safely");
129
130        assert!(
131            proposal.expiration_timestamp >= proposal.timestamp,
132            "expiration must not overflow below creation timestamp"
133        );
134    }
135}