Skip to content

Commit eef404b

Browse files
committed
Tiebreak TC tips by tip.qc
If a TC contains multiple tips with the same round, the tips should be tiebroken by tip.qc
1 parent dc35a40 commit eef404b

File tree

4 files changed

+208
-80
lines changed

4 files changed

+208
-80
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

monad-consensus-types/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ toml = { workspace = true }
2727
monad-bls = { workspace = true }
2828
monad-secp = { workspace = true }
2929
monad-testutil = { workspace = true }
30+
31+
test-case = { workspace = true }

monad-consensus-types/src/timeout.rs

Lines changed: 195 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -120,42 +120,9 @@ where
120120
EPT: ExecutionProtocol,
121121
{
122122
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
123-
match (&self, other) {
124-
(Self::Qc(qc_1), Self::Qc(qc_2)) => {
125-
if qc_1.get_round() == qc_2.get_round() {
126-
None
127-
} else {
128-
Some(qc_1.get_round().cmp(&qc_2.get_round()))
129-
}
130-
}
131-
(Self::Tip(tip_1), Self::Tip(tip_2)) => {
132-
if tip_1.block_header.block_round == tip_2.block_header.block_round {
133-
None
134-
} else {
135-
Some(
136-
tip_1
137-
.block_header
138-
.block_round
139-
.cmp(&tip_2.block_header.block_round),
140-
)
141-
}
142-
}
143-
(Self::Qc(qc_1), Self::Tip(tip_2)) => {
144-
if qc_1.get_round() == tip_2.block_header.block_round {
145-
// QC takes precedence
146-
Some(std::cmp::Ordering::Greater)
147-
} else {
148-
Some(qc_1.get_round().cmp(&tip_2.block_header.block_round))
149-
}
150-
}
151-
(Self::Tip(tip_1), Self::Qc(qc_2)) => {
152-
if tip_1.block_header.block_round == qc_2.get_round() {
153-
// QC takes precedence
154-
Some(std::cmp::Ordering::Less)
155-
} else {
156-
Some(tip_1.block_header.block_round.cmp(&qc_2.get_round()))
157-
}
158-
}
123+
match self.rank().cmp(&other.rank()) {
124+
std::cmp::Ordering::Equal if self != other => None,
125+
ord => Some(ord),
159126
}
160127
}
161128
}
@@ -166,6 +133,18 @@ where
166133
SCT: SignatureCollection<NodeIdPubKey = CertificateSignaturePubKey<ST>>,
167134
EPT: ExecutionProtocol,
168135
{
136+
pub fn rank(&self) -> HighExtendRank {
137+
match &self {
138+
Self::Tip(tip) => HighExtendRank::Tip {
139+
tip_round: tip.block_header.block_round,
140+
qc_round: tip.block_header.qc.get_round(),
141+
},
142+
Self::Qc(qc) => HighExtendRank::Qc {
143+
qc_round: qc.get_round(),
144+
},
145+
}
146+
}
147+
169148
pub fn qc(&self) -> &QuorumCertificate<SCT> {
170149
match &self {
171150
Self::Tip(tip) => &tip.block_header.qc,
@@ -238,6 +217,17 @@ where
238217
SCT: SignatureCollection<NodeIdPubKey = CertificateSignaturePubKey<ST>>,
239218
EPT: ExecutionProtocol,
240219
{
220+
pub fn rank(&self) -> HighExtendRank {
221+
match &self {
222+
Self::Tip(tip, _) => HighExtendRank::Tip {
223+
tip_round: tip.block_header.block_round,
224+
qc_round: tip.block_header.qc.get_round(),
225+
},
226+
Self::Qc(qc) => HighExtendRank::Qc {
227+
qc_round: qc.get_round(),
228+
},
229+
}
230+
}
241231
pub fn qc(&self) -> &QuorumCertificate<SCT> {
242232
match &self {
243233
Self::Tip(tip, _) => &tip.block_header.qc,
@@ -315,6 +305,81 @@ where
315305
}
316306
}
317307

308+
#[derive(PartialEq, Eq)]
309+
pub enum HighExtendRank {
310+
Tip { tip_round: Round, qc_round: Round },
311+
Qc { qc_round: Round },
312+
}
313+
314+
impl PartialOrd for HighExtendRank {
315+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
316+
Some(self.cmp(other))
317+
}
318+
}
319+
320+
impl Ord for HighExtendRank {
321+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
322+
match (&self, other) {
323+
(
324+
Self::Qc {
325+
qc_round: qc_round_1,
326+
},
327+
Self::Qc {
328+
qc_round: qc_round_2,
329+
},
330+
) => qc_round_1.cmp(qc_round_2),
331+
(
332+
Self::Tip {
333+
qc_round: qc_round_1,
334+
tip_round: tip_round_1,
335+
},
336+
Self::Tip {
337+
qc_round: qc_round_2,
338+
tip_round: tip_round_2,
339+
},
340+
) => {
341+
if tip_round_1 != tip_round_2 {
342+
tip_round_1.cmp(tip_round_2)
343+
} else {
344+
qc_round_1.cmp(qc_round_2)
345+
}
346+
}
347+
(
348+
Self::Qc {
349+
qc_round: qc_round_1,
350+
},
351+
Self::Tip {
352+
qc_round: _,
353+
tip_round: tip_round_2,
354+
},
355+
) => {
356+
if qc_round_1 == tip_round_2 {
357+
// QC takes precedence
358+
std::cmp::Ordering::Greater
359+
} else {
360+
qc_round_1.cmp(tip_round_2)
361+
}
362+
}
363+
(
364+
Self::Tip {
365+
qc_round: _,
366+
tip_round: tip_round_1,
367+
},
368+
Self::Qc {
369+
qc_round: qc_round_2,
370+
},
371+
) => {
372+
if tip_round_1 == qc_round_2 {
373+
// QC takes precedence
374+
std::cmp::Ordering::Less
375+
} else {
376+
tip_round_1.cmp(qc_round_2)
377+
}
378+
}
379+
}
380+
}
381+
}
382+
318383
/// Data to include in a timeout
319384
#[derive(Clone, Debug, PartialEq, Eq, Hash, RlpEncodable, RlpDecodable, Serialize)]
320385
pub struct TimeoutInfo {
@@ -329,6 +394,21 @@ pub struct TimeoutInfo {
329394
pub high_tip_round: Round,
330395
}
331396

397+
impl TimeoutInfo {
398+
pub fn rank(&self) -> HighExtendRank {
399+
if self.high_tip_round == GENESIS_ROUND {
400+
HighExtendRank::Qc {
401+
qc_round: self.high_qc_round,
402+
}
403+
} else {
404+
HighExtendRank::Tip {
405+
qc_round: self.high_qc_round,
406+
tip_round: self.high_tip_round,
407+
}
408+
}
409+
}
410+
}
411+
332412
#[derive(Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Serialize, Deserialize)]
333413
#[serde(bound(
334414
serialize = "SCT: SignatureCollection",
@@ -342,6 +422,21 @@ pub struct HighTipRoundSigColTuple<SCT> {
342422
pub sigs: SCT,
343423
}
344424

425+
impl<SCT> HighTipRoundSigColTuple<SCT> {
426+
pub fn rank(&self) -> HighExtendRank {
427+
if self.high_tip_round == GENESIS_ROUND {
428+
HighExtendRank::Qc {
429+
qc_round: self.high_qc_round,
430+
}
431+
} else {
432+
HighExtendRank::Tip {
433+
qc_round: self.high_qc_round,
434+
tip_round: self.high_tip_round,
435+
}
436+
}
437+
}
438+
}
439+
345440
/// TimeoutCertificate is used to advance rounds when a QC is unable to
346441
/// form for a round
347442
/// A collection of Timeout messages is the basis for building a TC
@@ -427,3 +522,67 @@ where
427522

428523
pub high_qc: QuorumCertificate<SCT>,
429524
}
525+
526+
#[cfg(test)]
527+
mod test {
528+
use std::cmp::Ordering;
529+
530+
use monad_types::Round;
531+
use test_case::test_case;
532+
533+
use crate::timeout::HighExtendRank;
534+
535+
#[test_case(
536+
HighExtendRank::Qc { qc_round: Round(1) },
537+
HighExtendRank::Qc { qc_round: Round(1) },
538+
Ordering::Equal;
539+
"qc equal"
540+
)]
541+
#[test_case(
542+
HighExtendRank::Qc { qc_round: Round(1) },
543+
HighExtendRank::Qc { qc_round: Round(2) },
544+
Ordering::Less;
545+
"qc not equal"
546+
)]
547+
#[test_case(
548+
HighExtendRank::Tip { tip_round: Round(1), qc_round: Round(0) },
549+
HighExtendRank::Tip { tip_round: Round(1), qc_round: Round(0) },
550+
Ordering::Equal;
551+
"tip equal"
552+
)]
553+
#[test_case(
554+
HighExtendRank::Tip { tip_round: Round(1), qc_round: Round(0) },
555+
HighExtendRank::Tip { tip_round: Round(2), qc_round: Round(0) },
556+
Ordering::Less;
557+
"tip not equal 1"
558+
)]
559+
#[test_case(
560+
HighExtendRank::Tip { tip_round: Round(1), qc_round: Round(0) },
561+
HighExtendRank::Tip { tip_round: Round(2), qc_round: Round(1) },
562+
Ordering::Less;
563+
"tip not equal 2"
564+
)]
565+
#[test_case(
566+
HighExtendRank::Tip { tip_round: Round(1), qc_round: Round(0) },
567+
HighExtendRank::Tip { tip_round: Round(1), qc_round: Round(1) },
568+
Ordering::Less;
569+
"tip rounds equal, qc rounds not equal"
570+
)]
571+
#[test_case(
572+
HighExtendRank::Tip { tip_round: Round(1), qc_round: Round(0) },
573+
HighExtendRank::Qc { qc_round: Round(1) },
574+
Ordering::Less;
575+
"tip round equals qc round"
576+
)]
577+
#[test_case(
578+
HighExtendRank::Tip { tip_round: Round(2), qc_round: Round(0) },
579+
HighExtendRank::Qc { qc_round: Round(1) },
580+
Ordering::Greater;
581+
"tip round greater than qc round"
582+
)]
583+
fn high_extend_rank(rank_1: HighExtendRank, rank_2: HighExtendRank, ord: Ordering) {
584+
assert_eq!(rank_1.cmp(&rank_2), ord);
585+
// test reverse
586+
assert_eq!(rank_2.cmp(&rank_1), ord.reverse());
587+
}
588+
}

monad-consensus/src/validation/signing.rs

Lines changed: 10 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use alloy_rlp::{Decodable, Encodable, RlpDecodable, RlpEncodable};
2222
use monad_consensus_types::{
2323
no_endorsement::{FreshProposalCertificate, NoEndorsementCertificate},
2424
quorum_certificate::QuorumCertificate,
25-
timeout::{HighExtend, HighExtendVote, NoTipCertificate, TimeoutCertificate, TimeoutInfo},
25+
timeout::{HighExtend, HighExtendRank, NoTipCertificate, TimeoutCertificate, TimeoutInfo},
2626
tip::ConsensusTip,
2727
validation::Error,
2828
RoundCertificate,
@@ -594,23 +594,8 @@ where
594594
},
595595
&timeout.high_extend.clone().into(),
596596
)?;
597-
match &timeout.high_extend {
598-
HighExtendVote::Qc(qc) => {
599-
if timeout.tminfo.high_tip_round != GENESIS_ROUND {
600-
return Err(Error::NotWellFormed);
601-
}
602-
if timeout.tminfo.high_qc_round != qc.get_round() {
603-
return Err(Error::NotWellFormed);
604-
}
605-
}
606-
HighExtendVote::Tip(tip, _) => {
607-
if timeout.tminfo.high_tip_round != tip.block_header.block_round {
608-
return Err(Error::NotWellFormed);
609-
}
610-
if timeout.tminfo.high_qc_round >= tip.block_header.block_round {
611-
return Err(Error::NotWellFormed);
612-
}
613-
}
597+
if timeout.high_extend.rank() != timeout.tminfo.rank() {
598+
return Err(Error::NotWellFormed);
614599
}
615600

616601
self.well_formed_timeout()?;
@@ -842,8 +827,9 @@ where
842827
let (validators, validator_mapping, _leader) = epoch_to_validators(tc.epoch, tc.round)?;
843828

844829
let mut node_ids = Vec::new();
845-
let mut highest_qc_round = GENESIS_ROUND;
846-
let mut highest_tip_round = GENESIS_ROUND;
830+
let mut highest_rank = HighExtendRank::Qc {
831+
qc_round: GENESIS_ROUND,
832+
};
847833
let mut seen_tip_rounds = HashSet::new();
848834
let mut seen_node_ids = HashSet::new();
849835
for t in tc.tip_rounds.iter() {
@@ -857,11 +843,8 @@ where
857843
return Err(Error::DuplicateTcTipRound);
858844
}
859845

860-
if t.high_qc_round > highest_qc_round {
861-
highest_qc_round = t.high_qc_round;
862-
}
863-
if t.high_tip_round > highest_tip_round {
864-
highest_tip_round = t.high_tip_round;
846+
if t.rank() > highest_rank {
847+
highest_rank = t.rank();
865848
}
866849

867850
let td = TimeoutInfo {
@@ -895,25 +878,8 @@ where
895878
}?;
896879

897880
verify_high_extend(cert_cache, epoch_to_validators, &tc.high_extend)?;
898-
match &tc.high_extend {
899-
HighExtend::Qc(qc) => {
900-
if qc.get_round() != highest_qc_round {
901-
return Err(Error::NotWellFormed);
902-
}
903-
if highest_tip_round > highest_qc_round {
904-
// higher tip exists
905-
return Err(Error::NotWellFormed);
906-
}
907-
}
908-
HighExtend::Tip(tip) => {
909-
if tip.block_header.block_round != highest_tip_round {
910-
return Err(Error::NotWellFormed);
911-
}
912-
if highest_qc_round >= highest_tip_round {
913-
// qc for same or higher round exists
914-
return Err(Error::NotWellFormed);
915-
}
916-
}
881+
if tc.high_extend.rank() != highest_rank {
882+
return Err(Error::NotWellFormed);
917883
}
918884

919885
cert_cache.cache_validated_tc(tc);

0 commit comments

Comments
 (0)