Rewrite tendermint's message handling loop to much more clearly match the paper (#560)

* Rewrite tendermint's message handling loop to much more clearly match the paper

No longer checks relevant branches upon messages, yet all branches upon any
state change. This is slower, yet easier to review and likely without one or
two rare edge cases.

When reviewing, please see page 5 of https://arxiv.org/pdf/1807.04938.pdf.
Lines from the specified algorithm can be found in the code by searching for
"// L".

* Sane rebroadcasting of consensus messages

Instead of broadcasting the last n messages on the Tributary side of things, we
now have the machine rebroadcast the message tape for the current block.

* Only rebroadcast messages which didn't error in some way

* Only rebroadcast our own messages for tendermint
This commit is contained in:
Luke Parker
2024-04-21 05:30:31 -04:00
committed by GitHub
parent fd4f247917
commit 523d2ac911
7 changed files with 555 additions and 528 deletions

View File

@@ -3,7 +3,6 @@ use std::{
collections::{HashSet, HashMap},
};
use parity_scale_codec::Encode;
use serai_db::{Get, DbTxn, Db};
use crate::{
@@ -20,7 +19,7 @@ pub(crate) struct BlockData<N: Network> {
pub(crate) number: BlockNumber,
pub(crate) validator_id: Option<N::ValidatorId>,
pub(crate) proposal: Option<N::Block>,
pub(crate) our_proposal: Option<N::Block>,
pub(crate) log: MessageLog<N>,
pub(crate) slashes: HashSet<N::ValidatorId>,
@@ -43,7 +42,7 @@ impl<N: Network> BlockData<N> {
weights: Arc<N::Weights>,
number: BlockNumber,
validator_id: Option<N::ValidatorId>,
proposal: Option<N::Block>,
our_proposal: Option<N::Block>,
) -> BlockData<N> {
BlockData {
db,
@@ -51,7 +50,7 @@ impl<N: Network> BlockData<N> {
number,
validator_id,
proposal,
our_proposal,
log: MessageLog::new(weights),
slashes: HashSet::new(),
@@ -108,17 +107,17 @@ impl<N: Network> BlockData<N> {
self.populate_end_time(round);
}
// 11-13
// L11-13
self.round = Some(RoundData::<N>::new(
round,
time.unwrap_or_else(|| self.end_time[&RoundNumber(round.0 - 1)]),
));
self.end_time.insert(round, self.round().end_time());
// 14-21
// L14-21
if Some(proposer) == self.validator_id {
let (round, block) = self.valid.clone().unzip();
block.or_else(|| self.proposal.clone()).map(|block| Data::Proposal(round, block))
block.or_else(|| self.our_proposal.clone()).map(|block| Data::Proposal(round, block))
} else {
self.round_mut().set_timeout(Step::Propose);
None
@@ -198,8 +197,8 @@ impl<N: Network> BlockData<N> {
assert!(!new_round);
None?;
}
// Put this message to the DB
txn.put(&msg_key, res.encode());
// Put that we're sending this message to the DB
txn.put(&msg_key, []);
txn.commit();
}

View File

@@ -288,7 +288,7 @@ pub trait Network: Sized + Send + Sync {
async fn slash(&mut self, validator: Self::ValidatorId, slash_event: SlashEvent);
/// Validate a block.
async fn validate(&mut self, block: &Self::Block) -> Result<(), BlockError>;
async fn validate(&self, block: &Self::Block) -> Result<(), BlockError>;
/// Add a block, returning the proposal for the next one.
///

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ use std::{sync::Arc, collections::HashMap};
use parity_scale_codec::Encode;
use crate::{ext::*, RoundNumber, Step, DataFor, TendermintError, SignedMessageFor, Evidence};
use crate::{ext::*, RoundNumber, Step, DataFor, SignedMessageFor, Evidence};
type RoundLog<N> = HashMap<<N as Network>::ValidatorId, HashMap<Step, SignedMessageFor<N>>>;
pub(crate) struct MessageLog<N: Network> {
@@ -16,7 +16,7 @@ impl<N: Network> MessageLog<N> {
}
// Returns true if it's a new message
pub(crate) fn log(&mut self, signed: SignedMessageFor<N>) -> Result<bool, TendermintError<N>> {
pub(crate) fn log(&mut self, signed: SignedMessageFor<N>) -> Result<bool, Evidence> {
let msg = &signed.msg;
// Clarity, and safety around default != new edge cases
let round = self.log.entry(msg.round).or_insert_with(HashMap::new);
@@ -30,10 +30,7 @@ impl<N: Network> MessageLog<N> {
target: "tendermint",
"Validator sent multiple messages for the same block + round + step"
);
Err(TendermintError::Malicious(
msg.sender,
Some(Evidence::ConflictingMessages(existing.encode(), signed.encode())),
))?;
Err(Evidence::ConflictingMessages(existing.encode(), signed.encode()))?;
}
return Ok(false);
}
@@ -47,7 +44,8 @@ impl<N: Network> MessageLog<N> {
pub(crate) fn message_instances(&self, round: RoundNumber, data: &DataFor<N>) -> (u64, u64) {
let mut participating = 0;
let mut weight = 0;
for (participant, msgs) in &self.log[&round] {
let Some(log) = self.log.get(&round) else { return (0, 0) };
for (participant, msgs) in log {
if let Some(msg) = msgs.get(&data.step()) {
let validator_weight = self.weights.weight(*participant);
participating += validator_weight;
@@ -73,7 +71,8 @@ impl<N: Network> MessageLog<N> {
// Check if a supermajority of nodes have participated on a specific step
pub(crate) fn has_participation(&self, round: RoundNumber, step: Step) -> bool {
let mut participating = 0;
for (participant, msgs) in &self.log[&round] {
let Some(log) = self.log.get(&round) else { return false };
for (participant, msgs) in log {
if msgs.get(&step).is_some() {
participating += self.weights.weight(*participant);
}

View File

@@ -145,7 +145,7 @@ impl Network for TestNetwork {
println!("Slash for {id} due to {event:?}");
}
async fn validate(&mut self, block: &TestBlock) -> Result<(), BlockError> {
async fn validate(&self, block: &TestBlock) -> Result<(), BlockError> {
block.valid
}