use hashgraph_like_consensus::{
protos::consensus::v1::{Proposal, Vote},
types::ConsensusEvent,
};
use crate::{
core::{CoreError, ScoreEvent, ScoreOp},
protos::de_mls::messages::v1::{
AppMessage, BanRequest, CommitCandidate, ConversationMessage, ConversationSync,
ConversationUpdateRequest, EmergencyCriteriaProposal, InvitationToJoin, Outcome,
ProposalAdded, RemoveMember, UserKeyPackage, UserVote, ViolationEvidence, ViolationType,
VotePayload, WelcomeMessage, app_message, conversation_update_request, welcome_message,
},
};
#[derive(Debug, Clone)]
pub enum ProcessResult {
AppMessage(Box<AppMessage>),
Proposal(Box<Proposal>),
Vote(Box<Vote>),
LeaveConversation,
MembershipChangeReceived(Box<ConversationUpdateRequest>),
JoinedConversation(String),
ConversationUpdated,
CommitCandidateReceived { steward: Vec<u8> },
ConversationSyncReceived(Box<ConversationSync>),
Noop(NoopReason),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NoopReason {
UnknownAppMessage,
FastPathRejected,
BanTargetNotMember,
DecryptIgnored,
UnexpectedMlsType,
NoApprovedProposals,
AlreadyCommitted,
EmptyCandidatePayload,
EmptyStewardIdentity,
WireKindMismatch,
SelectionLocked,
StaleEpoch,
DuplicateBufferedHash,
}
impl ViolationEvidence {
pub fn broken_commit(target: Vec<u8>, epoch: u64, payload: impl Into<Vec<u8>>) -> Self {
Self {
violation_type: ViolationType::BrokenCommit as i32,
target_member_id: target,
evidence_payload: payload.into(),
epoch,
creator_member_id: Vec::new(),
}
}
pub fn broken_mls_proposal(target: Vec<u8>, epoch: u64, payload: impl Into<Vec<u8>>) -> Self {
Self {
violation_type: ViolationType::BrokenMlsProposal as i32,
target_member_id: target,
evidence_payload: payload.into(),
epoch,
creator_member_id: Vec::new(),
}
}
pub fn censorship_inactivity(target: Vec<u8>, epoch: u64) -> Self {
Self {
violation_type: ViolationType::CensorshipInactivity as i32,
target_member_id: target,
evidence_payload: Vec::new(),
epoch,
creator_member_id: Vec::new(),
}
}
pub fn score_below_threshold(target: Vec<u8>, epoch: u64, current_score: i64) -> Self {
Self {
violation_type: ViolationType::ScoreBelowThreshold as i32,
target_member_id: target,
evidence_payload: current_score.to_le_bytes().to_vec(),
epoch,
creator_member_id: Vec::new(),
}
}
pub fn deadlock(epoch: u64) -> Self {
Self {
violation_type: ViolationType::Deadlock as i32,
target_member_id: Vec::new(),
evidence_payload: Vec::new(),
epoch,
creator_member_id: Vec::new(),
}
}
pub fn with_creator(mut self, creator: Vec<u8>) -> Self {
self.creator_member_id = creator;
self
}
pub fn into_update_request(self) -> Result<ConversationUpdateRequest, CoreError> {
if self.creator_member_id.is_empty() {
return Err(CoreError::InvalidConversationUpdateRequest);
}
Ok(ConversationUpdateRequest {
payload: Some(conversation_update_request::Payload::EmergencyCriteria(
EmergencyCriteriaProposal {
evidence: Some(self),
},
)),
})
}
pub fn target_score_event(&self) -> Option<ScoreEvent> {
match ViolationType::try_from(self.violation_type) {
Ok(ViolationType::BrokenCommit) => Some(ScoreEvent::BrokenCommit),
Ok(ViolationType::BrokenMlsProposal) => Some(ScoreEvent::BrokenMlsProposal),
Ok(ViolationType::CensorshipInactivity) => Some(ScoreEvent::CensorshipInactivity),
Ok(ViolationType::ScoreBelowThreshold)
| Ok(ViolationType::Deadlock)
| Ok(ViolationType::ViolationUnspecified)
| Err(_) => None,
}
}
pub fn target_score_op(&self) -> Option<ScoreOp> {
Some(ScoreOp {
member_id: self.target_member_id.clone(),
event: self.target_score_event()?,
})
}
}
macro_rules! impl_payload_from {
($envelope:ty, $( $inner:ty => $variant:path ),+ $(,)?) => {
$(
impl From<$inner> for $envelope {
fn from(value: $inner) -> Self {
Self { payload: Some($variant(value)) }
}
}
)+
};
}
impl_payload_from!(
WelcomeMessage,
UserKeyPackage => welcome_message::Payload::UserKeyPackage,
InvitationToJoin => welcome_message::Payload::InvitationToJoin,
);
impl_payload_from!(
AppMessage,
VotePayload => app_message::Payload::VotePayload,
UserVote => app_message::Payload::UserVote,
ConversationMessage => app_message::Payload::ConversationMessage,
CommitCandidate => app_message::Payload::CommitCandidate,
BanRequest => app_message::Payload::BanRequest,
Proposal => app_message::Payload::Proposal,
Vote => app_message::Payload::Vote,
ConversationSync => app_message::Payload::ConversationSync,
ProposalAdded => app_message::Payload::ProposalAdded,
);
impl From<ConsensusEvent> for Outcome {
fn from(ev: ConsensusEvent) -> Self {
match ev {
ConsensusEvent::ConsensusReached { result: true, .. } => Outcome::Accepted,
ConsensusEvent::ConsensusReached { result: false, .. } => Outcome::Rejected,
ConsensusEvent::ConsensusFailed { .. } => Outcome::Unspecified,
}
}
}
impl TryFrom<AppMessage> for ProcessResult {
type Error = CoreError;
fn try_from(value: AppMessage) -> Result<Self, Self::Error> {
match &value.payload {
Some(app_message::Payload::ConversationMessage(_)) => {
Ok(ProcessResult::AppMessage(Box::new(value)))
}
Some(app_message::Payload::Proposal(proposal)) => {
Ok(ProcessResult::Proposal(Box::new(proposal.clone())))
}
Some(app_message::Payload::Vote(vote)) => {
Ok(ProcessResult::Vote(Box::new(vote.clone())))
}
Some(app_message::Payload::BanRequest(ban_request)) => Ok(
ProcessResult::MembershipChangeReceived(Box::new(ConversationUpdateRequest {
payload: Some(conversation_update_request::Payload::RemoveMember(
RemoveMember {
identity: ban_request.user_to_ban.clone(),
},
)),
})),
),
Some(app_message::Payload::ConversationSync(sync)) => Ok(
ProcessResult::ConversationSyncReceived(Box::new(sync.clone())),
),
other => {
tracing::debug!(
payload_kind = ?other.as_ref().map(std::mem::discriminant),
"app message ignored: payload variant not consumed by core dispatch"
);
Ok(ProcessResult::Noop(NoopReason::UnknownAppMessage))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn broken_commit_evidence_roundtrips_into_update_request() {
let evidence = ViolationEvidence::broken_commit(vec![0xAA, 0xBB], 5, vec![0xDE, 0xAD])
.with_creator(vec![0x01]);
let request = evidence.into_update_request().unwrap();
let Some(conversation_update_request::Payload::EmergencyCriteria(ec)) = request.payload
else {
panic!("Expected EmergencyCriteria payload");
};
let ev = ec.evidence.expect("evidence present");
assert_eq!(ev.violation_type, ViolationType::BrokenCommit as i32);
assert_eq!(ev.target_member_id, vec![0xAA, 0xBB]);
assert_eq!(ev.epoch, 5);
assert_eq!(ev.evidence_payload, vec![0xDE, 0xAD]);
assert_eq!(ev.creator_member_id, vec![0x01]);
}
#[test]
fn into_update_request_errors_without_creator() {
let evidence = ViolationEvidence::broken_commit(vec![0xAA], 0, Vec::<u8>::new());
let err = evidence
.into_update_request()
.expect_err("creator required");
assert!(matches!(err, CoreError::InvalidConversationUpdateRequest));
}
}