1use hashgraph_like_consensus::{
5 protos::consensus::v1::{Proposal, Vote},
6 types::ConsensusEvent,
7};
8
9use crate::{
10 ConversationError, ScoreEvent, ScoreOp,
11 protos::de_mls::messages::v1::{
12 AppMessage, BanRequest, CommitCandidate, ConversationMessage, ConversationSync,
13 ConversationUpdateRequest, EmergencyCriteriaProposal, EventMembershipChange, MemberWelcome,
14 Outcome, ProposalAdded, UserVote, ViolationEvidence, ViolationType, VotePayload,
15 app_message, conversation_update_request,
16 },
17};
18
19#[derive(Debug, Clone)]
26pub enum ProcessResult {
27 AppMessage(Box<AppMessage>),
29
30 Proposal(Box<Proposal>),
32
33 Vote(Box<Vote>),
35
36 LeaveConversation,
38
39 MembershipChangeReceived(Box<ConversationUpdateRequest>),
41
42 ConversationUpdated,
44
45 CommitCandidateReceived { steward_id: Vec<u8> },
47
48 ConversationSyncReceived(Box<ConversationSync>),
50
51 WelcomeBroadcastReceived(Box<MemberWelcome>),
55
56 Noop(NoopReason),
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum NoopReason {
64 UnknownAppMessage,
66 FastPathRejected,
69 BanTargetNotMember,
71 DecryptIgnored,
73 UnexpectedMlsType,
75 NoApprovedProposals,
77 AlreadyCommitted,
79 EmptyCandidatePayload,
81 EmptyStewardMemberId,
83 WireKindMismatch,
85 DuplicateBufferedHash,
87 CandidateBufferFull,
89 CandidateStashedEarly,
92 EmptyWelcomePayload,
94 DuplicateWelcomeBroadcast,
96}
97
98impl ViolationEvidence {
101 pub fn broken_commit(target: Vec<u8>, epoch: u64, payload: impl Into<Vec<u8>>) -> Self {
104 Self {
105 violation_type: ViolationType::BrokenCommit as i32,
106 target_member_id: target,
107 evidence_payload: payload.into(),
108 epoch,
109 creator_member_id: Vec::new(),
110 }
111 }
112
113 pub fn broken_mls_proposal(target: Vec<u8>, epoch: u64, payload: impl Into<Vec<u8>>) -> Self {
116 Self {
117 violation_type: ViolationType::BrokenMlsProposal as i32,
118 target_member_id: target,
119 evidence_payload: payload.into(),
120 epoch,
121 creator_member_id: Vec::new(),
122 }
123 }
124
125 pub fn score_below_threshold(target: Vec<u8>, epoch: u64, current_score: i64) -> Self {
127 Self {
128 violation_type: ViolationType::ScoreBelowThreshold as i32,
129 target_member_id: target,
130 evidence_payload: current_score.to_le_bytes().to_vec(),
131 epoch,
132 creator_member_id: Vec::new(),
133 }
134 }
135
136 pub fn deadlock(epoch: u64) -> Self {
139 Self {
140 violation_type: ViolationType::Deadlock as i32,
141 target_member_id: Vec::new(),
142 evidence_payload: Vec::new(),
143 epoch,
144 creator_member_id: Vec::new(),
145 }
146 }
147
148 pub fn with_creator(mut self, creator: Vec<u8>) -> Self {
150 self.creator_member_id = creator;
151 self
152 }
153
154 pub fn into_update_request(self) -> Result<ConversationUpdateRequest, ConversationError> {
159 if self.creator_member_id.is_empty() {
160 return Err(ConversationError::InvalidConversationUpdateRequest);
161 }
162 Ok(ConversationUpdateRequest {
163 payload: Some(conversation_update_request::Payload::EmergencyCriteria(
164 EmergencyCriteriaProposal {
165 evidence: Some(self),
166 },
167 )),
168 })
169 }
170
171 pub fn target_score_event(&self) -> Option<ScoreEvent> {
176 match ViolationType::try_from(self.violation_type) {
177 Ok(ViolationType::BrokenCommit) => Some(ScoreEvent::BrokenCommit),
178 Ok(ViolationType::BrokenMlsProposal) => Some(ScoreEvent::BrokenMlsProposal),
179 Ok(ViolationType::CensorshipInactivity) => Some(ScoreEvent::CensorshipInactivity),
180 Ok(ViolationType::ScoreBelowThreshold)
181 | Ok(ViolationType::Deadlock)
182 | Ok(ViolationType::ViolationUnspecified)
183 | Err(_) => None,
184 }
185 }
186
187 pub fn target_score_op(&self) -> Option<ScoreOp> {
190 Some(ScoreOp {
191 member_id: self.target_member_id.clone(),
192 event: self.target_score_event()?,
193 })
194 }
195}
196
197macro_rules! impl_payload_from {
200 ($envelope:ty, $( $inner:ty => $variant:path ),+ $(,)?) => {
201 $(
202 impl From<$inner> for $envelope {
203 fn from(value: $inner) -> Self {
204 Self { payload: Some($variant(value)) }
205 }
206 }
207 )+
208 };
209}
210
211impl_payload_from!(
212 AppMessage,
213 VotePayload => app_message::Payload::VotePayload,
214 UserVote => app_message::Payload::UserVote,
215 ConversationMessage => app_message::Payload::ConversationMessage,
216 CommitCandidate => app_message::Payload::CommitCandidate,
217 BanRequest => app_message::Payload::BanRequest,
218 Proposal => app_message::Payload::Proposal,
219 Vote => app_message::Payload::Vote,
220 ConversationSync => app_message::Payload::ConversationSync,
221 ProposalAdded => app_message::Payload::ProposalAdded,
222 MemberWelcome => app_message::Payload::MemberWelcome,
223 EventMembershipChange => app_message::Payload::MembershipChange,
224);
225
226impl From<ConsensusEvent> for Outcome {
227 fn from(ev: ConsensusEvent) -> Self {
228 match ev {
229 ConsensusEvent::ConsensusReached { result: true, .. } => Outcome::Accepted,
230 ConsensusEvent::ConsensusReached { result: false, .. } => Outcome::Rejected,
231 ConsensusEvent::ConsensusFailed { .. } => Outcome::Unspecified,
232 }
233 }
234}
235
236impl TryFrom<AppMessage> for ProcessResult {
237 type Error = ConversationError;
238 fn try_from(value: AppMessage) -> Result<Self, Self::Error> {
239 match &value.payload {
240 Some(app_message::Payload::ConversationMessage(_)) => {
241 Ok(ProcessResult::AppMessage(Box::new(value)))
242 }
243 Some(app_message::Payload::MembershipChange(_)) => {
244 Ok(ProcessResult::AppMessage(Box::new(value)))
245 }
246 Some(app_message::Payload::Proposal(proposal)) => {
247 Ok(ProcessResult::Proposal(Box::new(proposal.clone())))
248 }
249 Some(app_message::Payload::Vote(vote)) => {
250 Ok(ProcessResult::Vote(Box::new(vote.clone())))
251 }
252 Some(app_message::Payload::BanRequest(ban_request)) => {
253 Ok(ProcessResult::MembershipChangeReceived(Box::new(
254 ConversationUpdateRequest::remove_member(ban_request.user_to_ban.clone()),
255 )))
256 }
257 Some(app_message::Payload::ConversationSync(sync)) => Ok(
258 ProcessResult::ConversationSyncReceived(Box::new(sync.clone())),
259 ),
260 other => {
261 tracing::debug!(
262 payload_kind = ?other.as_ref().map(std::mem::discriminant),
263 "app message ignored: payload variant not consumed by core dispatch"
264 );
265 Ok(ProcessResult::Noop(NoopReason::UnknownAppMessage))
266 }
267 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
279 fn broken_commit_evidence_roundtrips_into_update_request() {
280 let evidence = ViolationEvidence::broken_commit(vec![0xAA, 0xBB], 5, vec![0xDE, 0xAD])
281 .with_creator(vec![0x01]);
282 let request = evidence.into_update_request().unwrap();
283
284 let Some(conversation_update_request::Payload::EmergencyCriteria(ec)) = request.payload
285 else {
286 panic!("Expected EmergencyCriteria payload");
287 };
288 let ev = ec.evidence.expect("evidence present");
289 assert_eq!(ev.violation_type, ViolationType::BrokenCommit as i32);
290 assert_eq!(ev.target_member_id, vec![0xAA, 0xBB]);
291 assert_eq!(ev.epoch, 5);
292 assert_eq!(ev.evidence_payload, vec![0xDE, 0xAD]);
293 assert_eq!(ev.creator_member_id, vec![0x01]);
294 }
295
296 #[test]
298 fn into_update_request_errors_without_creator() {
299 let evidence = ViolationEvidence::broken_commit(vec![0xAA], 0, Vec::<u8>::new());
300 let err = evidence
301 .into_update_request()
302 .expect_err("creator required");
303 assert!(matches!(
304 err,
305 ConversationError::InvalidConversationUpdateRequest
306 ));
307 }
308}