Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions code/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions code/crates/app-channel/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub async fn start_engine<Node, Ctx, Codec>(
cfg: Node::Config,
start_height: Option<Ctx::Height>,
initial_validator_set: Ctx::ValidatorSet,
) -> Result<(Channels<Ctx>, EngineHandle)>
) -> Result<(Channels<Ctx>, EngineHandle<Ctx>)>
where
Ctx: Context,
Node: node::Node<Context = Ctx>,
Expand Down Expand Up @@ -80,7 +80,8 @@ where
)
.await?;

let (node, handle) = spawn_node_actor(ctx, network, consensus, wal, sync, connector).await?;
let (node, handle) =
spawn_node_actor(ctx, network, consensus.clone(), wal, sync, connector).await?;

let channels = Channels {
consensus: rx_consensus,
Expand All @@ -90,6 +91,7 @@ where

let handle = EngineHandle {
actor: node,
consensus,
handle,
};

Expand Down
5 changes: 4 additions & 1 deletion code/crates/app/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use std::path::PathBuf;

use async_trait::async_trait;
use malachitebft_engine::consensus::{ConsensusRef, Msg};
use rand::{CryptoRng, RngCore};
use serde::de::DeserializeOwned;
use serde::Serialize;
Expand All @@ -18,8 +19,9 @@ use malachitebft_engine::util::events::RxEvent;
use crate::types::core::{Context, PrivateKey, PublicKey, VotingPower};
use crate::types::Keypair;

pub struct EngineHandle {
pub struct EngineHandle<Ctx: Context> {
pub actor: NodeRef,
pub consensus: ConsensusRef<Ctx>,
pub handle: JoinHandle<()>,
}

Expand All @@ -31,6 +33,7 @@ where
{
fn subscribe(&self) -> RxEvent<Ctx>;
async fn kill(&self, reason: Option<String>) -> eyre::Result<()>;
fn inject(&self, message: Msg<Ctx>) -> eyre::Result<()>;
}

pub trait NodeConfig {
Expand Down
1 change: 1 addition & 0 deletions code/crates/core-consensus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ debug = ["std", "malachitebft-core-driver/debug"]
[dependencies]
malachitebft-core-types.workspace = true
malachitebft-core-driver.workspace = true
malachitebft-core-votekeeper.workspace = true
malachitebft-metrics = { workspace = true, optional = true }
malachitebft-peer.workspace = true

Expand Down
6 changes: 6 additions & 0 deletions code/crates/core-consensus/src/handle/decide.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::MisbehaviorEvidence;
use crate::{handle::signature::verify_commit_certificate, prelude::*};

#[cfg_attr(not(feature = "metrics"), allow(unused_variables))]
Expand Down Expand Up @@ -100,6 +101,11 @@ where
}
}

let _evidence = MisbehaviorEvidence {
proposals: state.driver.proposal_evidence().clone(),
votes: state.driver.vote_evidence().clone(),
};

perform!(
co,
Effect::Decide(certificate, extensions, Default::default())
Expand Down
12 changes: 12 additions & 0 deletions code/crates/core-consensus/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,15 @@ pub enum VoteExtensionError {
#[error("Invalid vote extension")]
InvalidVoteExtension,
}

#[derive_where(Clone, Debug)]
pub struct MisbehaviorEvidence<Ctx: Context> {
pub proposals: malachitebft_core_driver::EvidenceMap<Ctx>,
pub votes: malachitebft_core_votekeeper::EvidenceMap<Ctx>,
}

impl<Ctx: Context> MisbehaviorEvidence<Ctx> {
pub fn is_empty(&self) -> bool {
self.proposals.is_empty() && self.votes.is_empty()
}
}
1 change: 1 addition & 0 deletions code/crates/core-driver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ malachitebft-core-votekeeper = { workspace = true }

derive-where = { workspace = true }
thiserror = { workspace = true, default-features = false }
tracing = { workspace = true }

[dev-dependencies]
malachitebft-test = { workspace = true }
17 changes: 12 additions & 5 deletions code/crates/core-driver/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use malachitebft_core_votekeeper::keeper::VoteKeeper;

use crate::input::Input;
use crate::output::Output;
use crate::proposal_keeper::{EvidenceMap, ProposalKeeper};
use crate::proposal_keeper::{self, ProposalKeeper};
use crate::Error;
use crate::ThresholdParams;

Expand Down Expand Up @@ -192,11 +192,16 @@ where
&self.validator_set
}

/// Return recorded evidence of equivocation for this height.
pub fn evidence(&self) -> &EvidenceMap<Ctx> {
/// Return recorded evidence of proposal equivocation for this height.
pub fn proposal_evidence(&self) -> &proposal_keeper::EvidenceMap<Ctx> {
self.proposal_keeper.evidence()
}

/// Return recorded evidence of vote equivocation for this height.
pub fn vote_evidence(&self) -> &malachitebft_core_votekeeper::EvidenceMap<Ctx> {
self.votes().evidence()
}

/// Return the proposer for the current round.
pub fn get_proposer(&self) -> Result<&Ctx::Validator, Error<Ctx>> {
if let Some(proposer) = &self.proposer {
Expand Down Expand Up @@ -449,8 +454,10 @@ where
let vote_round = vote.round();
let this_round = self.round();

let Some(output) = self.vote_keeper.apply_vote(vote, this_round) else {
return Ok(None);
let output = match self.vote_keeper.apply_vote(vote, this_round) {
Ok(Some(output)) => output,
Ok(None) => return Ok(None),
Err(_) => return Ok(None),
};

if let VKOutput::PolkaValue(val) = &output {
Expand Down
1 change: 1 addition & 0 deletions code/crates/core-driver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ pub use driver::Driver;
pub use error::Error;
pub use input::Input;
pub use output::Output;
pub use proposal_keeper::EvidenceMap;

pub use malachitebft_core_votekeeper::ThresholdParams;
3 changes: 2 additions & 1 deletion code/crates/core-driver/src/mux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ where
let proposal = signed_proposal.message.clone();

// Store the proposal and its validity
self.proposal_keeper
let _ = self
.proposal_keeper
.store_proposal(signed_proposal, validity);

self.multiplex_proposal(proposal, validity)
Expand Down
48 changes: 43 additions & 5 deletions code/crates/core-driver/src/proposal_keeper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use derive_where::derive_where;
use thiserror::Error;

use malachitebft_core_types::{Context, Proposal, Round, SignedProposal, Validity};
use tracing::warn;

/// Errors can that be yielded when recording a proposal.
#[derive_where(Debug)]
Expand Down Expand Up @@ -126,18 +127,32 @@ where
///
/// # Precondition
/// - The given proposal must have been proposed by the expected proposer at the proposal's height and round.
pub fn store_proposal(&mut self, proposal: SignedProposal<Ctx>, validity: Validity) {
pub fn store_proposal(
&mut self,
proposal: SignedProposal<Ctx>,
validity: Validity,
) -> Result<(), RecordProposalError<Ctx>> {
let per_round = self.per_round.entry(proposal.round()).or_default();

match per_round.add(proposal, validity) {
Ok(()) => (),
Ok(()) => Ok(()),

Err(RecordProposalError::ConflictingProposal {
existing,
conflicting,
}) => {
// This is an equivocating proposal
self.evidence.add(existing, conflicting);
self.evidence.add(existing.clone(), conflicting.clone());

warn!(
"Conflicting proposal: existing: {:?}, conflicting: {:?}",
existing, conflicting
);

Err(RecordProposalError::ConflictingProposal {
existing,
conflicting,
})
}

Err(RecordProposalError::InvalidConflictingProposal {
Expand All @@ -163,6 +178,7 @@ where
{
#[allow(clippy::type_complexity)]
map: BTreeMap<Ctx::Address, Vec<(SignedProposal<Ctx>, SignedProposal<Ctx>)>>,
last: Option<(Ctx::Address, (SignedProposal<Ctx>, SignedProposal<Ctx>))>,
}

impl<Ctx> EvidenceMap<Ctx>
Expand All @@ -187,6 +203,20 @@ where
self.map.get(address)
}

/// Check if the given proposal is the last equivocation recorded. If it is, return the
/// address of the validator and the evidence.
pub fn is_last_equivocation(
&self,
proposal: &SignedProposal<Ctx>,
) -> Option<(Ctx::Address, (SignedProposal<Ctx>, SignedProposal<Ctx>))> {
self.last
.as_ref()
.filter(|(address, (_, conflicting))| {
address == proposal.validator_address() && conflicting == proposal
})
.cloned()
}

/// Add evidence of equivocating proposals, ie. two proposals submitted by the same validator,
/// but with different values but for the same height and round.
///
Expand All @@ -199,12 +229,20 @@ where
);

if let Some(evidence) = self.map.get_mut(conflicting.validator_address()) {
evidence.push((existing, conflicting));
evidence.push((existing.clone(), conflicting.clone()));
self.last = Some((
conflicting.validator_address().clone(),
(existing, conflicting),
));
} else {
self.map.insert(
conflicting.validator_address().clone(),
vec![(existing, conflicting)],
vec![(existing.clone(), conflicting.clone())],
);
self.last = Some((
conflicting.validator_address().clone(),
(existing, conflicting),
));
}
}
}
1 change: 1 addition & 0 deletions code/crates/core-votekeeper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ malachitebft-core-types = { workspace = true }

derive-where = { workspace = true }
thiserror = { workspace = true, default-features = false }
tracing = { workspace = true }

[dev-dependencies]
malachitebft-test = { workspace = true }
46 changes: 39 additions & 7 deletions code/crates/core-votekeeper/src/evidence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ where
{
#[allow(clippy::type_complexity)]
map: BTreeMap<Ctx::Address, Vec<(SignedVote<Ctx>, SignedVote<Ctx>)>>,
last: Option<(Ctx::Address, (SignedVote<Ctx>, SignedVote<Ctx>))>,
}

impl<Ctx> EvidenceMap<Ctx>
Expand All @@ -36,15 +37,46 @@ where
self.map.get(address)
}

/// Add evidence of equivocation.
pub fn add(&mut self, existing: SignedVote<Ctx>, vote: SignedVote<Ctx>) {
debug_assert_eq!(existing.validator_address(), vote.validator_address());
/// Check if the given vote is the last equivocation recorded. If it is, return the
/// address of the validator and the evidence.
pub fn is_last_equivocation(
&self,
vote: &SignedVote<Ctx>,
) -> Option<(Ctx::Address, (SignedVote<Ctx>, SignedVote<Ctx>))> {
self.last
.as_ref()
.filter(|(address, (_, conflicting))| {
address == vote.validator_address() && conflicting == vote
})
.cloned()
}

/// Add evidence of equivocating votes, ie. two votes submitted by the same validator,
/// but with different values but for the same height and round.
///
/// # Precondition
/// - Panics if the two conflicting votes were not proposed by the same validator.
pub fn add(&mut self, existing: SignedVote<Ctx>, conflicting: SignedVote<Ctx>) {
debug_assert_eq!(
existing.validator_address(),
conflicting.validator_address()
);

if let Some(evidence) = self.map.get_mut(vote.validator_address()) {
evidence.push((existing, vote));
if let Some(evidence) = self.map.get_mut(conflicting.validator_address()) {
evidence.push((existing.clone(), conflicting.clone()));
self.last = Some((
conflicting.validator_address().clone(),
(existing, conflicting),
));
} else {
self.map
.insert(vote.validator_address().clone(), vec![(existing, vote)]);
self.map.insert(
conflicting.validator_address().clone(),
vec![(existing.clone(), conflicting.clone())],
);
self.last = Some((
conflicting.validator_address().clone(),
(existing, conflicting),
));
}
}
}
Loading
Loading