@@ -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.
10841139func (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
0 commit comments