@@ -264,20 +264,41 @@ func (i *instance) receiveOne(msg *GMessage) (bool, error) {
264
264
}
265
265
case PREPARE_PHASE :
266
266
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
+ }
267
275
case COMMIT_PHASE :
268
276
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.
272
281
if ! msg .Vote .Value .IsZero () {
273
282
msgRound .committed .ReceiveJustification (msg .Vote .Value , msg .Justification )
274
283
}
275
284
// Every COMMIT phase stays open to new messages even after the protocol moves on
276
285
// to a new round. Late-arriving COMMITs can still (must) cause a local decision,
277
286
// *in that round*. Try to complete the COMMIT phase for the round specified by
278
287
// 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.
279
293
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
+ }
281
302
}
282
303
case DECIDE_PHASE :
283
304
i .decision .Receive (msg .Sender , msg .Vote .Value , msg .Signature )
@@ -296,8 +317,7 @@ func (i *instance) postReceive(roundsReceived ...uint64) {
296
317
// Check whether the instance should skip ahead to future round, in descending order.
297
318
slices .Reverse (roundsReceived )
298
319
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 {
301
321
i .skipToRound (r , chain , justification )
302
322
return
303
323
}
@@ -309,7 +329,8 @@ func (i *instance) postReceive(roundsReceived ...uint64) {
309
329
// proposal. Otherwise, it returns nil chain, nil justification and false.
310
330
//
311
331
// 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 )
313
334
// Check if the given round is ahead of current round and this instance is not in
314
335
// DECIDE phase.
315
336
if round <= i .current .Round || i .current .Phase == DECIDE_PHASE {
@@ -497,19 +518,29 @@ func (i *instance) tryPrepare() error {
497
518
return fmt .Errorf ("unexpected phase %s, expected %s" , i .current .Phase , PREPARE_PHASE )
498
519
}
499
520
500
- prepared := i .getRound (i .current .Round ).prepared
521
+ currentRound := i .getRound (i .current .Round )
522
+ prepared := currentRound .prepared
501
523
proposalKey := i .proposal .Key ()
502
524
foundQuorum := prepared .HasStrongQuorumFor (proposalKey )
503
525
quorumNotPossible := ! prepared .CouldReachStrongQuorumFor (proposalKey , false )
504
526
phaseComplete := i .phaseTimeoutElapsed () && prepared .ReceivedFromStrongQuorum ()
505
527
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 {
507
538
i .value = i .proposal
508
539
} else if quorumNotPossible || phaseComplete {
509
540
i .value = & ECChain {}
510
541
}
511
542
512
- if foundQuorum || quorumNotPossible || phaseComplete {
543
+ if foundQuorum || foundJustification || quorumNotPossible || phaseComplete {
513
544
i .beginCommit ()
514
545
} else if i .shouldRebroadcast () {
515
546
i .tryRebroadcast ()
@@ -528,9 +559,18 @@ func (i *instance) beginCommit() {
528
559
// No justification is required for committing bottom.
529
560
var justification * Justification
530
561
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 {
532
566
// Found a strong quorum of PREPARE, build the justification for it.
533
567
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
534
574
} else {
535
575
panic ("beginCommit with no strong quorum for non-bottom value" )
536
576
}
@@ -549,6 +589,15 @@ func (i *instance) tryCommit(round uint64) error {
549
589
quorumValue , foundStrongQuorum := committed .FindStrongQuorumValue ()
550
590
phaseComplete := i .phaseTimeoutElapsed () && committed .ReceivedFromStrongQuorum ()
551
591
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
+
552
601
switch {
553
602
case foundStrongQuorum && ! quorumValue .IsZero ():
554
603
// 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 {
559
608
case i .current .Round != round , i .current .Phase != COMMIT_PHASE :
560
609
// We are at a phase other than COMMIT or round does not match the current one;
561
610
// nothing else to do.
562
- case foundStrongQuorum :
611
+ case foundStrongQuorum , foundJustificationForBottom :
563
612
// There is a strong quorum for bottom, carry forward the existing proposal.
564
613
i .beginNextRound ()
565
614
case phaseComplete :
@@ -658,17 +707,23 @@ func (i *instance) beginNextRound() {
658
707
i .current .Round += 1
659
708
metrics .currentRound .Record (context .TODO (), int64 (i .current .Round ))
660
709
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 ()
662
713
// Proposal was updated at the end of COMMIT phase to be some value for which
663
714
// this node received a COMMIT message (bearing justification), if there were any.
664
715
// If there were none, there must have been a strong quorum for bottom instead.
665
716
var justification * Justification
666
- if quorum , ok := prevRound .committed .FindStrongQuorumFor (bottomECChain . Key () ); ok {
717
+ if quorum , ok := previousRound .committed .FindStrongQuorumFor (bottomKey ); ok {
667
718
// Build justification for strong quorum of COMMITs for bottom in the previous round.
668
719
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
669
724
} else {
670
725
// 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 ()]
672
727
if ! ok {
673
728
panic ("beginConverge called but no justification for proposal" )
674
729
}
@@ -1080,12 +1135,45 @@ func (q *quorumState) ReceivedFromWeakQuorum() bool {
1080
1135
return hasWeakQuorum (q .sendersTotalPower , q .powerTable .ScaledTotal )
1081
1136
}
1082
1137
1083
- // Checks whether a chain has reached a strong quorum.
1138
+ // HasStrongQuorumFor checks whether a chain has reached a strong quorum.
1084
1139
func (q * quorumState ) HasStrongQuorumFor (key ECChainKey ) bool {
1085
1140
supportForChain , ok := q .chainSupport [key ]
1086
1141
return ok && supportForChain .hasStrongQuorum
1087
1142
}
1088
1143
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
+
1089
1177
// CouldReachStrongQuorumFor checks whether the given chain can possibly reach
1090
1178
// strong quorum given the locally received messages.
1091
1179
// 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 {
1338
1426
return ConvergeValue {}
1339
1427
}
1340
1428
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
+
1341
1461
///// General helpers /////
1342
1462
1343
1463
// The only messages that are spammable are COMMIT for bottom. QUALITY and
0 commit comments