Skip to content

Commit f9c95ae

Browse files
authored
Infer messages from evidence of justification (#1024)
* Infer messages from evidence of justification The justification accompanied by most messages in GPBFT is evidence of a strong quorum of some states across the participants. Treat the receipt of such justifications as the receipt of the messages that justified it. This allows a faster progress towards quorum in the network, and reduces reliance on re-broadcast and majority network state propagation. Specifically, there are 2 phases that can benefit from the above optimization: 1. PREPARE phase, using justification of PREPARE in: * COMMIT messages at the same round. * PREPARE messages at the next round. * CONVERGE messages at the next round. 2. COMMIT phase, using justification of COMMIT to bottom in: * CONVERGE messages at the next round. * PREPARE messages at the next round. The above optimization doesn't matter in CONVERGE and DECIDE phases. Because, CONVERGE by design awaits the phase timeout by design, and DECIDE is already optimized by `skipToDecide` where the participant rightly so immediately progresses to the final phase using the justification of COMMIT accompanied by the DECIDE message. Fixes #342 * Add tests and fix logic flow in evaluating current round for COMMIT Change the message processing logic to proceed to evaluate the current round if a COMMIT message could make a difference to PREPARE completion. Add emulator tests to assert PREPARE and COMMIT phases complete immediately if the right justification arrives.
1 parent 6e00604 commit f9c95ae

File tree

3 files changed

+258
-17
lines changed

3 files changed

+258
-17
lines changed

gpbft/gpbft.go

Lines changed: 136 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -264,20 +264,41 @@ func (i *instance) receiveOne(msg *GMessage) (bool, error) {
264264
}
265265
case PREPARE_PHASE:
266266
msgRound.prepared.Receive(msg.Sender, msg.Vote.Value, msg.Signature)
267+
268+
// All PREPARE messages beyond round zero carry either justification of COMMIT
269+
// for bottom or PREPARE for vote value from their previous round. Collect such
270+
// justifications to potentially advance the current round at COMMIT or PREPARE
271+
// by reusing the same justification as evidence of strong quorum.
272+
if msg.Justification != nil {
273+
msgRound.prepared.ReceiveJustification(msg.Vote.Value, msg.Justification)
274+
}
267275
case COMMIT_PHASE:
268276
msgRound.committed.Receive(msg.Sender, msg.Vote.Value, msg.Signature)
269-
// The only justifications that need to be stored for future propagation are for COMMITs
270-
// to non-bottom values.
271-
// This evidence can be brought forward to justify a CONVERGE message in the next round.
277+
// The only justifications that need to be stored for future propagation are for
278+
// COMMITs to non-bottom values. This evidence can be brought forward to justify
279+
// a CONVERGE message in the next round, or justify progress from PREPARE in the
280+
// current round.
272281
if !msg.Vote.Value.IsZero() {
273282
msgRound.committed.ReceiveJustification(msg.Vote.Value, msg.Justification)
274283
}
275284
// Every COMMIT phase stays open to new messages even after the protocol moves on
276285
// to a new round. Late-arriving COMMITs can still (must) cause a local decision,
277286
// *in that round*. Try to complete the COMMIT phase for the round specified by
278287
// the message.
288+
//
289+
// Otherwise, if the COMMIT message hasn't resulted in progress of the current
290+
// round, continue to try the current phase. Because, the COMMIT message may
291+
// justify PREPARE in the current round if the participant is currently in
292+
// PREPARE phase.
279293
if i.current.Phase != DECIDE_PHASE {
280-
return true, i.tryCommit(msg.Vote.Round)
294+
err := i.tryCommit(msg.Vote.Round)
295+
// Proceed to attempt to complete the current phase only if the COMMIT message
296+
// could potentially justify the current round's PREPARE phase. Otherwise,
297+
// there's no point in trying to complete the current phase.
298+
tryToCompleteCurrentPhase := err == nil && i.current.Phase == PREPARE_PHASE && i.current.Round == msg.Vote.Round && !msg.Vote.Value.IsZero()
299+
if !tryToCompleteCurrentPhase {
300+
return true, err
301+
}
281302
}
282303
case DECIDE_PHASE:
283304
i.decision.Receive(msg.Sender, msg.Vote.Value, msg.Signature)
@@ -296,8 +317,7 @@ func (i *instance) postReceive(roundsReceived ...uint64) {
296317
// Check whether the instance should skip ahead to future round, in descending order.
297318
slices.Reverse(roundsReceived)
298319
for _, r := range roundsReceived {
299-
round := i.getRound(r)
300-
if chain, justification, skip := i.shouldSkipToRound(r, round); skip {
320+
if chain, justification, skip := i.shouldSkipToRound(r); skip {
301321
i.skipToRound(r, chain, justification)
302322
return
303323
}
@@ -309,7 +329,8 @@ func (i *instance) postReceive(roundsReceived ...uint64) {
309329
// proposal. Otherwise, it returns nil chain, nil justification and false.
310330
//
311331
// See: skipToRound.
312-
func (i *instance) shouldSkipToRound(round uint64, state *roundState) (*ECChain, *Justification, bool) {
332+
func (i *instance) shouldSkipToRound(round uint64) (*ECChain, *Justification, bool) {
333+
state := i.getRound(round)
313334
// Check if the given round is ahead of current round and this instance is not in
314335
// DECIDE phase.
315336
if round <= i.current.Round || i.current.Phase == DECIDE_PHASE {
@@ -497,19 +518,29 @@ func (i *instance) tryPrepare() error {
497518
return fmt.Errorf("unexpected phase %s, expected %s", i.current.Phase, PREPARE_PHASE)
498519
}
499520

500-
prepared := i.getRound(i.current.Round).prepared
521+
currentRound := i.getRound(i.current.Round)
522+
prepared := currentRound.prepared
501523
proposalKey := i.proposal.Key()
502524
foundQuorum := prepared.HasStrongQuorumFor(proposalKey)
503525
quorumNotPossible := !prepared.CouldReachStrongQuorumFor(proposalKey, false)
504526
phaseComplete := i.phaseTimeoutElapsed() && prepared.ReceivedFromStrongQuorum()
505527

506-
if foundQuorum {
528+
// Check if the proposal has been justified by COMMIT messages at the current
529+
// round or PREPARE/CONVERGE message from the next round. This indicates that
530+
// there exists a strong quorum of PREPARE for the proposal at the current round
531+
// but hasn't been seen yet by this participant.
532+
nextRound := i.getRound(i.current.Round + 1)
533+
foundJustification := currentRound.committed.HasJustificationOf(PREPARE_PHASE, proposalKey) ||
534+
nextRound.prepared.HasJustificationOf(PREPARE_PHASE, proposalKey) ||
535+
nextRound.converged.HasJustificationOf(PREPARE_PHASE, proposalKey)
536+
537+
if foundQuorum || foundJustification {
507538
i.value = i.proposal
508539
} else if quorumNotPossible || phaseComplete {
509540
i.value = &ECChain{}
510541
}
511542

512-
if foundQuorum || quorumNotPossible || phaseComplete {
543+
if foundQuorum || foundJustification || quorumNotPossible || phaseComplete {
513544
i.beginCommit()
514545
} else if i.shouldRebroadcast() {
515546
i.tryRebroadcast()
@@ -528,9 +559,18 @@ func (i *instance) beginCommit() {
528559
// No justification is required for committing bottom.
529560
var justification *Justification
530561
if !i.value.IsZero() {
531-
if quorum, ok := i.getRound(i.current.Round).prepared.FindStrongQuorumFor(i.value.Key()); ok {
562+
valueKey := i.value.Key()
563+
currentRound := i.getRound(i.current.Round)
564+
nextRound := i.getRound(i.current.Round + 1)
565+
if quorum, ok := currentRound.prepared.FindStrongQuorumFor(valueKey); ok {
532566
// Found a strong quorum of PREPARE, build the justification for it.
533567
justification = i.buildJustification(quorum, i.current.Round, PREPARE_PHASE, i.value)
568+
} else if justifiedByCommit := currentRound.committed.GetJustificationOf(PREPARE_PHASE, valueKey); justifiedByCommit != nil {
569+
justification = justifiedByCommit
570+
} else if justifiedByNextPrepare := nextRound.prepared.GetJustificationOf(PREPARE_PHASE, valueKey); justifiedByNextPrepare != nil {
571+
justification = justifiedByNextPrepare
572+
} else if justifiedByNextConverge := nextRound.converged.GetJustificationOf(PREPARE_PHASE, valueKey); justifiedByNextConverge != nil {
573+
justification = justifiedByNextConverge
534574
} else {
535575
panic("beginCommit with no strong quorum for non-bottom value")
536576
}
@@ -549,6 +589,15 @@ func (i *instance) tryCommit(round uint64) error {
549589
quorumValue, foundStrongQuorum := committed.FindStrongQuorumValue()
550590
phaseComplete := i.phaseTimeoutElapsed() && committed.ReceivedFromStrongQuorum()
551591

592+
nextRound := i.getRound(round + 1)
593+
bottomKey := bottomECChain.Key()
594+
// Check for justification of COMMIT for bottom that may have been received in
595+
// a message from the next round at PREPARE or CONVERGE phases. This indicates a
596+
// strong quorum of COMMIT for bottom across the participants at current round
597+
// even if this participant hasn't yet seen it.
598+
foundJustificationForBottom := nextRound.prepared.HasJustificationOf(COMMIT_PHASE, bottomKey) ||
599+
nextRound.converged.HasJustificationOf(COMMIT_PHASE, bottomKey)
600+
552601
switch {
553602
case foundStrongQuorum && !quorumValue.IsZero():
554603
// There is a strong quorum for a non-zero value; accept it. A participant may be
@@ -559,7 +608,7 @@ func (i *instance) tryCommit(round uint64) error {
559608
case i.current.Round != round, i.current.Phase != COMMIT_PHASE:
560609
// We are at a phase other than COMMIT or round does not match the current one;
561610
// nothing else to do.
562-
case foundStrongQuorum:
611+
case foundStrongQuorum, foundJustificationForBottom:
563612
// There is a strong quorum for bottom, carry forward the existing proposal.
564613
i.beginNextRound()
565614
case phaseComplete:
@@ -658,17 +707,23 @@ func (i *instance) beginNextRound() {
658707
i.current.Round += 1
659708
metrics.currentRound.Record(context.TODO(), int64(i.current.Round))
660709

661-
prevRound := i.getRound(i.current.Round - 1)
710+
currentRound := i.getRound(i.current.Round)
711+
previousRound := i.getRound(i.current.Round - 1)
712+
bottomKey := bottomECChain.Key()
662713
// Proposal was updated at the end of COMMIT phase to be some value for which
663714
// this node received a COMMIT message (bearing justification), if there were any.
664715
// If there were none, there must have been a strong quorum for bottom instead.
665716
var justification *Justification
666-
if quorum, ok := prevRound.committed.FindStrongQuorumFor(bottomECChain.Key()); ok {
717+
if quorum, ok := previousRound.committed.FindStrongQuorumFor(bottomKey); ok {
667718
// Build justification for strong quorum of COMMITs for bottom in the previous round.
668719
justification = i.buildJustification(quorum, i.current.Round-1, COMMIT_PHASE, nil)
720+
} else if bottomJustifiedByPrepare := currentRound.prepared.GetJustificationOf(COMMIT_PHASE, bottomKey); bottomJustifiedByPrepare != nil {
721+
justification = bottomJustifiedByPrepare
722+
} else if bottomJustifiedByConverge := currentRound.converged.GetJustificationOf(COMMIT_PHASE, bottomKey); bottomJustifiedByConverge != nil {
723+
justification = bottomJustifiedByConverge
669724
} else {
670725
// Extract the justification received from some participant (possibly this node itself).
671-
justification, ok = prevRound.committed.receivedJustification[i.proposal.Key()]
726+
justification, ok = previousRound.committed.receivedJustification[i.proposal.Key()]
672727
if !ok {
673728
panic("beginConverge called but no justification for proposal")
674729
}
@@ -1080,12 +1135,45 @@ func (q *quorumState) ReceivedFromWeakQuorum() bool {
10801135
return hasWeakQuorum(q.sendersTotalPower, q.powerTable.ScaledTotal)
10811136
}
10821137

1083-
// Checks whether a chain has reached a strong quorum.
1138+
// HasStrongQuorumFor checks whether a chain has reached a strong quorum.
10841139
func (q *quorumState) HasStrongQuorumFor(key ECChainKey) bool {
10851140
supportForChain, ok := q.chainSupport[key]
10861141
return ok && supportForChain.hasStrongQuorum
10871142
}
10881143

1144+
// HasJustificationOf checks whether a justification for a chain key exists.
1145+
//
1146+
// See: GetJustificationOf for details on how the key is interpreted.
1147+
func (q *quorumState) HasJustificationOf(phase Phase, key ECChainKey) bool {
1148+
return q.GetJustificationOf(phase, key) != nil
1149+
}
1150+
1151+
// GetJustificationOf gets the justification for a chain or nil if no such
1152+
// justification exists. The given key may be zero, in which case the first
1153+
// justification for bottom that is found is returned.
1154+
func (q *quorumState) GetJustificationOf(phase Phase, key ECChainKey) *Justification {
1155+
1156+
// The justification vote value is either zero or matches the vote value. If the
1157+
// given key is zero, it indicates that the ask is for the justification of
1158+
// bottom. Iterate through the list of received justification to find a
1159+
// match.
1160+
//
1161+
// Otherwise, simply use the receivedJustification map keyed by vote value.
1162+
if key.IsZero() {
1163+
for _, justification := range q.receivedJustification {
1164+
if justification.Vote.Value.IsZero() && justification.Vote.Phase == phase {
1165+
return justification
1166+
}
1167+
}
1168+
return nil
1169+
}
1170+
justification, found := q.receivedJustification[key]
1171+
if found && justification.Vote.Phase == phase {
1172+
return justification
1173+
}
1174+
return nil
1175+
}
1176+
10891177
// CouldReachStrongQuorumFor checks whether the given chain can possibly reach
10901178
// strong quorum given the locally received messages.
10911179
// If withAdversary is true, an additional ⅓ of total power is added to the possible support,
@@ -1338,6 +1426,38 @@ func (c *convergeState) FindProposalFor(chain *ECChain) ConvergeValue {
13381426
return ConvergeValue{}
13391427
}
13401428

1429+
// HasJustificationOf checks whether a justification for a chain key exists.
1430+
//
1431+
// See: GetJustificationOf for details on how the key is interpreted.
1432+
func (c *convergeState) HasJustificationOf(phase Phase, key ECChainKey) bool {
1433+
return c.GetJustificationOf(phase, key) != nil
1434+
}
1435+
1436+
// GetJustificationOf gets the justification for a chain or nil if no such
1437+
// justification exists. The given key may be zero, in which case the first
1438+
// justification for bottom that is found is returned.
1439+
func (c *convergeState) GetJustificationOf(phase Phase, key ECChainKey) *Justification {
1440+
1441+
// The justification vote value is either zero or matches the vote value. If the
1442+
// given key is zero, it indicates that the ask is for the justification of
1443+
// bottom. Iterate through the converge values to find a match.
1444+
//
1445+
// Otherwise, simply use the values map keyed by vote value.
1446+
if key.IsZero() {
1447+
for _, value := range c.values {
1448+
if value.Justification.Vote.Value.IsZero() && value.Justification.Vote.Phase == phase {
1449+
return value.Justification
1450+
}
1451+
}
1452+
return nil
1453+
}
1454+
value, found := c.values[key]
1455+
if found && value.Justification.Vote.Phase == phase {
1456+
return value.Justification
1457+
}
1458+
return nil
1459+
}
1460+
13411461
///// General helpers /////
13421462

13431463
// The only messages that are spammable are COMMIT for bottom. QUALITY and

gpbft/gpbft_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,123 @@ func TestGPBFT_WithEvenPowerDistribution(t *testing.T) {
568568
driver.RequireConverge(13, instance.Proposal(), justification)
569569
driver.RequireNoBroadcast() // Nothing else should be broadcast until the next alarm.
570570
})
571+
572+
t.Run("When in PREPARE at round 0", func(t *testing.T) {
573+
// Advances the instance to PREPARE at round zero.
574+
whenInPrepareAtRoundZero := func(t *testing.T, instance *emulator.Instance, driver *emulator.Driver) {
575+
driver.RequireStartInstance(instance.ID())
576+
driver.RequireQuality()
577+
driver.RequireNoBroadcast()
578+
driver.RequireDeliverMessage(&gpbft.GMessage{
579+
Sender: 1,
580+
Vote: instance.NewQuality(instance.Proposal()),
581+
})
582+
driver.RequireDeliverAlarm()
583+
driver.RequirePrepare(instance.Proposal())
584+
driver.RequireNoBroadcast()
585+
}
586+
t.Run("Justification of COMMIT completes phase", func(t *testing.T) {
587+
instance, driver := newInstanceAndDriver(t)
588+
whenInPrepareAtRoundZero(t, instance, driver)
589+
590+
// At this point, sender 0 is in PREPARE phase for the instance proposal. Now,
591+
// send a COMMIT message carrying justification of PREPARE from all participants,
592+
// which should complete the PREPARE phase immediately even though sender 0
593+
// hasn't seen a strong quorum for the instance proposal via PREPARE messages.
594+
evidenceOfPrepare := instance.NewJustification(0, gpbft.PREPARE_PHASE, instance.Proposal(), 0, 1)
595+
driver.RequireDeliverMessage(&gpbft.GMessage{
596+
Sender: 1,
597+
Vote: instance.NewCommit(0, instance.Proposal()),
598+
Justification: evidenceOfPrepare,
599+
})
600+
driver.RequireCommit(0, instance.Proposal(), evidenceOfPrepare)
601+
})
602+
t.Run("Justification of PREPARE from next round completes phase", func(t *testing.T) {
603+
instance, driver := newInstanceAndDriver(t)
604+
whenInPrepareAtRoundZero(t, instance, driver)
605+
606+
// At this point, sender 0 is in PREPARE phase for the instance proposal. Now,
607+
// send a PREPARE message from round 1 carrying justification of PREPARE from all
608+
// participants in round 0, which should complete the PREPARE phase immediately
609+
// even though sender 0 hasn't seen a strong quorum for the instance proposal via
610+
// PREPARE messages.
611+
evidenceOfPrepare := instance.NewJustification(0, gpbft.PREPARE_PHASE, instance.Proposal(), 0, 1)
612+
driver.RequireDeliverMessage(&gpbft.GMessage{
613+
Sender: 1,
614+
Vote: instance.NewPrepare(1, instance.Proposal()),
615+
Justification: evidenceOfPrepare,
616+
})
617+
driver.RequireCommit(0, instance.Proposal(), evidenceOfPrepare)
618+
})
619+
t.Run("Justification of CONVERGE from next round completes phase", func(t *testing.T) {
620+
instance, driver := newInstanceAndDriver(t)
621+
whenInPrepareAtRoundZero(t, instance, driver)
622+
623+
// At this point, sender 0 is in PREPARE phase for the instance proposal. Now,
624+
// send a CONVERGE message from round 1 carrying justification of PREPARE from all
625+
// participants in round 0, which should complete the PREPARE phase immediately
626+
// even though sender 0 hasn't seen a strong quorum for the instance proposal via
627+
// PREPARE messages.
628+
evidenceOfPrepare := instance.NewJustification(0, gpbft.PREPARE_PHASE, instance.Proposal(), 0, 1)
629+
driver.RequireDeliverMessage(&gpbft.GMessage{
630+
Sender: 1,
631+
Vote: instance.NewConverge(1, instance.Proposal()),
632+
Ticket: emulator.ValidTicket,
633+
Justification: evidenceOfPrepare,
634+
})
635+
driver.RequireCommit(0, instance.Proposal(), evidenceOfPrepare)
636+
})
637+
})
638+
639+
t.Run("When in COMMIT for base at round 0", func(t *testing.T) {
640+
// Advances the instance to COMMIT for base decision at round zero.
641+
whenInCommitForBaseAtRoundZero := func(t *testing.T, instance *emulator.Instance, driver *emulator.Driver) {
642+
driver.RequireStartInstance(instance.ID())
643+
driver.RequireQuality()
644+
driver.RequireNoBroadcast()
645+
// Timeout QUALITY phase immediately without delivering any QUALITY messages.
646+
driver.RequireDeliverAlarm()
647+
// Assert PREPARE phase for base decision and progress PREPARE to commit.
648+
driver.RequirePrepare(instance.Proposal().BaseChain())
649+
driver.RequireDeliverMessage(&gpbft.GMessage{
650+
Sender: 1,
651+
Vote: instance.NewPrepare(0, instance.Proposal().BaseChain()),
652+
})
653+
// Assert COMMIT phase for base decision.
654+
driver.RequireCommit(0, instance.Proposal().BaseChain(), instance.NewJustification(0, gpbft.PREPARE_PHASE, instance.Proposal(), 0, 1))
655+
}
656+
t.Run("Justification of CONVERGE to bottom from next round completes phase", func(t *testing.T) {
657+
instance, driver := newInstanceAndDriver(t)
658+
whenInCommitForBaseAtRoundZero(t, instance, driver)
659+
660+
// At this point, sender 0 is in the COMMIT phase for the instance proposal. Now,
661+
// send a CONVERGE message from the next round carrying justification as evidence
662+
// of COMMIT to the bottom, which should complete the COMMIT phase immediately.
663+
evidenceOfCommitToBottom := instance.NewJustification(0, gpbft.COMMIT_PHASE, &gpbft.ECChain{}, 0, 1)
664+
driver.RequireDeliverMessage(&gpbft.GMessage{
665+
Sender: 1,
666+
Vote: instance.NewConverge(1, instance.Proposal().BaseChain()),
667+
Ticket: emulator.ValidTicket,
668+
Justification: evidenceOfCommitToBottom,
669+
})
670+
driver.RequireConverge(1, instance.Proposal().BaseChain(), evidenceOfCommitToBottom)
671+
})
672+
t.Run("Justification of PREPARE for bottom from next round completes phase", func(t *testing.T) {
673+
instance, driver := newInstanceAndDriver(t)
674+
whenInCommitForBaseAtRoundZero(t, instance, driver)
675+
676+
// At this point, sender 0 is in the COMMIT phase for the instance proposal. Now,
677+
// send a PREPARE message from the next round carrying justification as evidence
678+
// of COMMIT to the bottom, which should complete the COMMIT phase immediately.
679+
evidenceOfCommitToBottom := instance.NewJustification(0, gpbft.COMMIT_PHASE, &gpbft.ECChain{}, 0, 1)
680+
driver.RequireDeliverMessage(&gpbft.GMessage{
681+
Sender: 1,
682+
Vote: instance.NewPrepare(1, instance.Proposal().BaseChain()),
683+
Justification: evidenceOfCommitToBottom,
684+
})
685+
driver.RequireConverge(1, instance.Proposal().BaseChain(), evidenceOfCommitToBottom)
686+
})
687+
})
571688
}
572689

573690
func TestGPBFT_WithExactOneThirdToTwoThirdPowerDistribution(t *testing.T) {

test/drop_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ func TestDrop_ReachesConsensusDespiteMessageLoss(t *testing.T) {
4848
sim.WithIgnoreConsensusFor(dropAdversaryTarget))
4949
sm, err := sim.NewSimulation(opts...)
5050
require.NoError(t, err)
51-
require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe())
51+
// Run the simulation for 1 extra instance to give enough time to the drop
52+
// adversary target to complete the instance. Ideally, simulation should take a
53+
// function as the condition to stop the simulation. But for now a quick extra
54+
// round would do the trick.
55+
require.NoErrorf(t, sm.Run(instanceCount+1, maxRounds), "%s", sm.Describe())
5256
chain := ecChainGenerator.GenerateECChain(instanceCount-1, &gpbft.TipSet{}, math.MaxUint64)
5357
requireConsensusAtInstance(t, sm, instanceCount-1, chain.Head())
5458
})

0 commit comments

Comments
 (0)