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}