From 59e86ba65ad7a29280bcba76528cb209d9c321c2 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Fri, 14 Nov 2025 23:39:24 +0100 Subject: [PATCH 1/5] fix: added missing test cases in TestAdvanceView --- protocol/synchronizer/synchronizer_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/protocol/synchronizer/synchronizer_test.go b/protocol/synchronizer/synchronizer_test.go index 506df213..c7195750 100644 --- a/protocol/synchronizer/synchronizer_test.go +++ b/protocol/synchronizer/synchronizer_test.go @@ -181,6 +181,7 @@ func TestAdvanceView(t *testing.T) { {name: "signers=4/Simple___/__/TC/AC", tr: S, qc: F, tc: T, ac: T, firstSignerIdx: 0, wantView: 2}, {name: "signers=4/Simple___/QC/__/__", tr: S, qc: T, tc: F, ac: F, firstSignerIdx: 0, wantView: 2}, {name: "signers=4/Simple___/QC/__/AC", tr: S, qc: T, tc: F, ac: T, firstSignerIdx: 0, wantView: 2}, + {name: "signers=4/Simple___/QC/TC/__", tr: S, qc: T, tc: T, ac: F, firstSignerIdx: 0, wantView: 2}, {name: "signers=4/Simple___/QC/TC/AC", tr: S, qc: T, tc: T, ac: T, firstSignerIdx: 0, wantView: 2}, {name: "signers=4/Aggregate/__/__/__", tr: A, qc: F, tc: F, ac: F, firstSignerIdx: 0, wantView: 1}, // empty syncInfo, should not advance view {name: "signers=4/Aggregate/__/__/AC", tr: A, qc: F, tc: F, ac: T, firstSignerIdx: 0, wantView: 2}, @@ -188,14 +189,16 @@ func TestAdvanceView(t *testing.T) { {name: "signers=4/Aggregate/__/TC/AC", tr: A, qc: F, tc: T, ac: T, firstSignerIdx: 0, wantView: 2}, {name: "signers=4/Aggregate/QC/__/__", tr: A, qc: T, tc: F, ac: F, firstSignerIdx: 0, wantView: 1}, // aggregate timeout rule ignores quorum cert, will not advance view {name: "signers=4/Aggregate/QC/__/AC", tr: A, qc: T, tc: F, ac: T, firstSignerIdx: 0, wantView: 2}, + {name: "signers=4/Aggregate/QC/TC/__", tr: A, qc: T, tc: T, ac: F, firstSignerIdx: 0, wantView: 2}, {name: "signers=4/Aggregate/QC/TC/AC", tr: A, qc: T, tc: T, ac: T, firstSignerIdx: 0, wantView: 2}, - // three signers; quorum reacted, advance view + // three signers; quorum reached, advance view {name: "signers=3/Simple___/__/__/__", tr: S, qc: F, tc: F, ac: F, firstSignerIdx: 1, wantView: 1}, // empty syncInfo, should not advance view {name: "signers=3/Simple___/__/__/AC", tr: S, qc: F, tc: F, ac: T, firstSignerIdx: 1, wantView: 1}, // simple timeout rule ignores aggregate timeout cert, will not advance view {name: "signers=3/Simple___/__/TC/__", tr: S, qc: F, tc: T, ac: F, firstSignerIdx: 1, wantView: 2}, {name: "signers=3/Simple___/__/TC/AC", tr: S, qc: F, tc: T, ac: T, firstSignerIdx: 1, wantView: 2}, {name: "signers=3/Simple___/QC/__/__", tr: S, qc: T, tc: F, ac: F, firstSignerIdx: 1, wantView: 2}, {name: "signers=3/Simple___/QC/__/AC", tr: S, qc: T, tc: F, ac: T, firstSignerIdx: 1, wantView: 2}, + {name: "signers=3/Simple___/QC/TC/__", tr: S, qc: T, tc: T, ac: F, firstSignerIdx: 1, wantView: 2}, {name: "signers=3/Simple___/QC/TC/AC", tr: S, qc: T, tc: T, ac: T, firstSignerIdx: 1, wantView: 2}, {name: "signers=3/Aggregate/__/__/__", tr: A, qc: F, tc: F, ac: F, firstSignerIdx: 1, wantView: 1}, // empty syncInfo, should not advance view {name: "signers=3/Aggregate/__/__/AC", tr: A, qc: F, tc: F, ac: T, firstSignerIdx: 1, wantView: 2}, @@ -203,6 +206,7 @@ func TestAdvanceView(t *testing.T) { {name: "signers=3/Aggregate/__/TC/AC", tr: A, qc: F, tc: T, ac: T, firstSignerIdx: 1, wantView: 2}, {name: "signers=3/Aggregate/QC/__/__", tr: A, qc: T, tc: F, ac: F, firstSignerIdx: 1, wantView: 1}, // aggregate timeout rule ignores quorum cert, will not advance view {name: "signers=3/Aggregate/QC/__/AC", tr: A, qc: T, tc: F, ac: T, firstSignerIdx: 1, wantView: 2}, + {name: "signers=3/Aggregate/QC/TC/__", tr: A, qc: T, tc: T, ac: F, firstSignerIdx: 1, wantView: 2}, {name: "signers=3/Aggregate/QC/TC/AC", tr: A, qc: T, tc: T, ac: T, firstSignerIdx: 1, wantView: 2}, // only two signers; no quorum reached, should not advance view {name: "signers=2/Simple___/__/__/__", tr: S, qc: F, tc: F, ac: F, firstSignerIdx: 2, wantView: 1}, // empty syncInfo, should not advance view @@ -211,6 +215,7 @@ func TestAdvanceView(t *testing.T) { {name: "signers=2/Simple___/__/TC/AC", tr: S, qc: F, tc: T, ac: T, firstSignerIdx: 2, wantView: 1}, {name: "signers=2/Simple___/QC/__/__", tr: S, qc: T, tc: F, ac: F, firstSignerIdx: 2, wantView: 1}, {name: "signers=2/Simple___/QC/__/AC", tr: S, qc: T, tc: F, ac: T, firstSignerIdx: 2, wantView: 1}, + {name: "signers=2/Simple___/QC/TC/__", tr: S, qc: T, tc: T, ac: F, firstSignerIdx: 2, wantView: 1}, {name: "signers=2/Simple___/QC/TC/AC", tr: S, qc: T, tc: T, ac: T, firstSignerIdx: 2, wantView: 1}, {name: "signers=2/Aggregate/__/__/__", tr: A, qc: F, tc: F, ac: F, firstSignerIdx: 2, wantView: 1}, // empty syncInfo, should not advance view {name: "signers=2/Aggregate/__/__/AC", tr: A, qc: F, tc: F, ac: T, firstSignerIdx: 2, wantView: 1}, @@ -218,6 +223,7 @@ func TestAdvanceView(t *testing.T) { {name: "signers=2/Aggregate/__/TC/AC", tr: A, qc: F, tc: T, ac: T, firstSignerIdx: 2, wantView: 1}, {name: "signers=2/Aggregate/QC/__/__", tr: A, qc: T, tc: F, ac: F, firstSignerIdx: 2, wantView: 1}, {name: "signers=2/Aggregate/QC/__/AC", tr: A, qc: T, tc: F, ac: T, firstSignerIdx: 2, wantView: 1}, + {name: "signers=2/Aggregate/QC/TC/__", tr: A, qc: T, tc: T, ac: F, firstSignerIdx: 2, wantView: 1}, {name: "signers=2/Aggregate/QC/TC/AC", tr: A, qc: T, tc: T, ac: T, firstSignerIdx: 2, wantView: 1}, } for _, tt := range tests { From cab0a0183c3e17a6ed65c3cd17a17c5a313851a5 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Fri, 14 Nov 2025 23:41:50 +0100 Subject: [PATCH 2/5] refactor: move VerifySyncInfo from TimeoutRuler to cert.Authority This moves the VerifySyncInfo logic from the simple and aggregate timeout rulers into a common logic on the cert.Authority struct, along with many other crypto verification methods (also used by the VerifySyncInfo method). This also removes the method from the TimeoutRuler interface, reducing the interface to only two methods. --- protocol/synchronizer/synchronizer.go | 2 +- .../synchronizer/timeoutrule_aggregate.go | 22 +----------- protocol/synchronizer/timeoutrule_simple.go | 23 +----------- protocol/synchronizer/timeoutrules.go | 1 - security/cert/auth.go | 35 +++++++++++++++++++ 5 files changed, 38 insertions(+), 45 deletions(-) diff --git a/protocol/synchronizer/synchronizer.go b/protocol/synchronizer/synchronizer.go index 728ca15e..510f4ed8 100644 --- a/protocol/synchronizer/synchronizer.go +++ b/protocol/synchronizer/synchronizer.go @@ -250,7 +250,7 @@ func (s *Synchronizer) OnNewView(newView hotstuff.NewViewMsg) { func (s *Synchronizer) advanceView(syncInfo hotstuff.SyncInfo) { s.logger.Debugf("advanceView: %v", syncInfo) - qc, view, timeout, err := s.timeoutRules.VerifySyncInfo(syncInfo) + qc, view, timeout, err := s.auth.VerifySyncInfo(syncInfo) if err != nil { s.logger.Infof("advanceView: Failed to verify sync info: %v", err) return diff --git a/protocol/synchronizer/timeoutrule_aggregate.go b/protocol/synchronizer/timeoutrule_aggregate.go index 9e9b1270..96b84734 100644 --- a/protocol/synchronizer/timeoutrule_aggregate.go +++ b/protocol/synchronizer/timeoutrule_aggregate.go @@ -61,25 +61,5 @@ func (s *Aggregate) RemoteTimeoutRule(currentView, timeoutView hotstuff.View, ti return si, nil } -func (s *Aggregate) VerifySyncInfo(syncInfo hotstuff.SyncInfo) (qc *hotstuff.QuorumCert, view hotstuff.View, timeout bool, err error) { - if timeoutCert, haveTC := syncInfo.TC(); haveTC { - if err := s.auth.VerifyTimeoutCert(timeoutCert); err != nil { - return nil, 0, timeout, fmt.Errorf("failed to verify timeout certificate: %w", err) - } - view = timeoutCert.View() - timeout = true - } - if aggQC, haveQC := syncInfo.AggQC(); haveQC { - highQC, err := s.auth.VerifyAggregateQC(aggQC) - if err != nil { - return nil, 0, timeout, fmt.Errorf("failed to verify aggregate quorum certificate: %w", err) - } - if aggQC.View() >= view { - view = aggQC.View() - timeout = true - } - return &highQC, view, timeout, nil - } - return nil, view, timeout, nil // aggregate quorum certificate not present, so no high QC available -} +var _ TimeoutRuler = (*Aggregate)(nil) diff --git a/protocol/synchronizer/timeoutrule_simple.go b/protocol/synchronizer/timeoutrule_simple.go index 01667c51..7971e7c1 100644 --- a/protocol/synchronizer/timeoutrule_simple.go +++ b/protocol/synchronizer/timeoutrule_simple.go @@ -46,25 +46,4 @@ func (s *Simple) RemoteTimeoutRule(_, timeoutView hotstuff.View, timeouts []hots return hotstuff.NewSyncInfoWith(tc), nil } -func (s *Simple) VerifySyncInfo(syncInfo hotstuff.SyncInfo) (qc *hotstuff.QuorumCert, view hotstuff.View, timeout bool, err error) { - if timeoutCert, haveTC := syncInfo.TC(); haveTC { - if err := s.auth.VerifyTimeoutCert(timeoutCert); err != nil { - return nil, 0, timeout, fmt.Errorf("failed to verify timeout certificate: %w", err) - } - view = timeoutCert.View() - timeout = true - } - - if quorumCert, haveQC := syncInfo.QC(); haveQC { - if err := s.auth.VerifyQuorumCert(quorumCert); err != nil { - return nil, 0, timeout, fmt.Errorf("failed to verify quorum certificate: %w", err) - } - // if there is both a TC and a QC, we use the QC if its view is greater or equal to the TC. - if quorumCert.View() >= view { - view = quorumCert.View() - timeout = false - } - return &quorumCert, view, timeout, nil - } - return nil, view, timeout, nil // quorum certificate not present, so no high QC available -} +var _ TimeoutRuler = (*Simple)(nil) diff --git a/protocol/synchronizer/timeoutrules.go b/protocol/synchronizer/timeoutrules.go index c8fa7854..0c9379be 100644 --- a/protocol/synchronizer/timeoutrules.go +++ b/protocol/synchronizer/timeoutrules.go @@ -17,5 +17,4 @@ func NewTimeoutRuler(cfg *core.RuntimeConfig, auth *cert.Authority) TimeoutRuler type TimeoutRuler interface { LocalTimeoutRule(hotstuff.View, hotstuff.SyncInfo) (*hotstuff.TimeoutMsg, error) RemoteTimeoutRule(currentView, timeoutView hotstuff.View, timeouts []hotstuff.TimeoutMsg) (hotstuff.SyncInfo, error) - VerifySyncInfo(hotstuff.SyncInfo) (qc *hotstuff.QuorumCert, view hotstuff.View, timeout bool, err error) } diff --git a/security/cert/auth.go b/security/cert/auth.go index 7c21b01f..11a28664 100644 --- a/security/cert/auth.go +++ b/security/cert/auth.go @@ -209,3 +209,38 @@ func (c *Authority) VerifyAnyQC(proposal *hotstuff.ProposeMsg) error { } return c.VerifyQuorumCert(qc) } + +// VerifySyncInfo verifies the sync info and returns the highest QC found (if any), +// the highest view, whether it was a timeout, and an error if verification failed. +func (c *Authority) VerifySyncInfo(syncInfo hotstuff.SyncInfo) (qc *hotstuff.QuorumCert, view hotstuff.View, timeout bool, err error) { + if timeoutCert, haveTC := syncInfo.TC(); haveTC { + if err := c.VerifyTimeoutCert(timeoutCert); err != nil { + return nil, 0, timeout, fmt.Errorf("failed to verify timeout certificate: %w", err) + } + view = timeoutCert.View() + timeout = true + } + + if aggQC, haveQC := syncInfo.AggQC(); haveQC { + highQC, err := c.VerifyAggregateQC(aggQC) + if err != nil { + return nil, 0, timeout, fmt.Errorf("failed to verify aggregate quorum certificate: %w", err) + } + view = max(view, aggQC.View()) + timeout = true // timeout is true here since AggQC represents a timeout + return &highQC, view, timeout, nil + + } else if qc, haveQC := syncInfo.QC(); haveQC { + if err := c.VerifyQuorumCert(qc); err != nil { + return nil, 0, timeout, fmt.Errorf("failed to verify quorum certificate: %w", err) + } + // use the QC's view if greater or equal to the TC's view; in which case, it's not a timeout. + if qc.View() >= view { + view = qc.View() + timeout = false + } + return &qc, view, timeout, nil + } + + return nil, view, timeout, nil // no high QC available +} From 92d073bbc535939cbe1658db046522c2ed849847 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Fri, 14 Nov 2025 23:46:28 +0100 Subject: [PATCH 3/5] fix: update TestAdvanceView test cases to match new VerifySyncInfo Since the Simple and Aggregate timeout rules now share a common VerifySyncInfo method, the tests no longer matched the previous expectation. However, I believe these changes match the expecations for advancing the view when we have either and AC or QC. --- protocol/synchronizer/synchronizer_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/protocol/synchronizer/synchronizer_test.go b/protocol/synchronizer/synchronizer_test.go index c7195750..88d384b3 100644 --- a/protocol/synchronizer/synchronizer_test.go +++ b/protocol/synchronizer/synchronizer_test.go @@ -176,7 +176,7 @@ func TestAdvanceView(t *testing.T) { }{ // four signers; quorum reached, advance view {name: "signers=4/Simple___/__/__/__", tr: S, qc: F, tc: F, ac: F, firstSignerIdx: 0, wantView: 1}, // empty syncInfo, should not advance view - {name: "signers=4/Simple___/__/__/AC", tr: S, qc: F, tc: F, ac: T, firstSignerIdx: 0, wantView: 1}, // simple timeout rule ignores aggregate timeout cert, will not advance view + {name: "signers=4/Simple___/__/__/AC", tr: S, qc: F, tc: F, ac: T, firstSignerIdx: 0, wantView: 2}, // advance view; simple timeout rule no longer ignores aggregate certs: cert.Authority.VerifySyncInfo() {name: "signers=4/Simple___/__/TC/__", tr: S, qc: F, tc: T, ac: F, firstSignerIdx: 0, wantView: 2}, {name: "signers=4/Simple___/__/TC/AC", tr: S, qc: F, tc: T, ac: T, firstSignerIdx: 0, wantView: 2}, {name: "signers=4/Simple___/QC/__/__", tr: S, qc: T, tc: F, ac: F, firstSignerIdx: 0, wantView: 2}, @@ -187,13 +187,13 @@ func TestAdvanceView(t *testing.T) { {name: "signers=4/Aggregate/__/__/AC", tr: A, qc: F, tc: F, ac: T, firstSignerIdx: 0, wantView: 2}, {name: "signers=4/Aggregate/__/TC/__", tr: A, qc: F, tc: T, ac: F, firstSignerIdx: 0, wantView: 2}, {name: "signers=4/Aggregate/__/TC/AC", tr: A, qc: F, tc: T, ac: T, firstSignerIdx: 0, wantView: 2}, - {name: "signers=4/Aggregate/QC/__/__", tr: A, qc: T, tc: F, ac: F, firstSignerIdx: 0, wantView: 1}, // aggregate timeout rule ignores quorum cert, will not advance view + {name: "signers=4/Aggregate/QC/__/__", tr: A, qc: T, tc: F, ac: F, firstSignerIdx: 0, wantView: 2}, // advance view; aggregate timeout rule no longer ignores quorum certs: cert.Authority.VerifySyncInfo() {name: "signers=4/Aggregate/QC/__/AC", tr: A, qc: T, tc: F, ac: T, firstSignerIdx: 0, wantView: 2}, {name: "signers=4/Aggregate/QC/TC/__", tr: A, qc: T, tc: T, ac: F, firstSignerIdx: 0, wantView: 2}, {name: "signers=4/Aggregate/QC/TC/AC", tr: A, qc: T, tc: T, ac: T, firstSignerIdx: 0, wantView: 2}, // three signers; quorum reached, advance view {name: "signers=3/Simple___/__/__/__", tr: S, qc: F, tc: F, ac: F, firstSignerIdx: 1, wantView: 1}, // empty syncInfo, should not advance view - {name: "signers=3/Simple___/__/__/AC", tr: S, qc: F, tc: F, ac: T, firstSignerIdx: 1, wantView: 1}, // simple timeout rule ignores aggregate timeout cert, will not advance view + {name: "signers=3/Simple___/__/__/AC", tr: S, qc: F, tc: F, ac: T, firstSignerIdx: 1, wantView: 2}, // advance view; simple timeout rule no longer ignores aggregate certs: cert.Authority.VerifySyncInfo() {name: "signers=3/Simple___/__/TC/__", tr: S, qc: F, tc: T, ac: F, firstSignerIdx: 1, wantView: 2}, {name: "signers=3/Simple___/__/TC/AC", tr: S, qc: F, tc: T, ac: T, firstSignerIdx: 1, wantView: 2}, {name: "signers=3/Simple___/QC/__/__", tr: S, qc: T, tc: F, ac: F, firstSignerIdx: 1, wantView: 2}, @@ -204,7 +204,7 @@ func TestAdvanceView(t *testing.T) { {name: "signers=3/Aggregate/__/__/AC", tr: A, qc: F, tc: F, ac: T, firstSignerIdx: 1, wantView: 2}, {name: "signers=3/Aggregate/__/TC/__", tr: A, qc: F, tc: T, ac: F, firstSignerIdx: 1, wantView: 2}, {name: "signers=3/Aggregate/__/TC/AC", tr: A, qc: F, tc: T, ac: T, firstSignerIdx: 1, wantView: 2}, - {name: "signers=3/Aggregate/QC/__/__", tr: A, qc: T, tc: F, ac: F, firstSignerIdx: 1, wantView: 1}, // aggregate timeout rule ignores quorum cert, will not advance view + {name: "signers=3/Aggregate/QC/__/__", tr: A, qc: T, tc: F, ac: F, firstSignerIdx: 1, wantView: 2}, // advance view; aggregate timeout rule no longer ignores quorum certs: cert.Authority.VerifySyncInfo() {name: "signers=3/Aggregate/QC/__/AC", tr: A, qc: T, tc: F, ac: T, firstSignerIdx: 1, wantView: 2}, {name: "signers=3/Aggregate/QC/TC/__", tr: A, qc: T, tc: T, ac: F, firstSignerIdx: 1, wantView: 2}, {name: "signers=3/Aggregate/QC/TC/AC", tr: A, qc: T, tc: T, ac: T, firstSignerIdx: 1, wantView: 2}, From 2cccddfe0a553e3ba78951b2038bad6597b081be Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Sat, 15 Nov 2025 00:35:56 +0100 Subject: [PATCH 4/5] fix: VerifySyncInfo to return the highest QC among AggQC and QC This makes VerifySyncInfo pick the highest QC among AggQC and QC if SyncInfo contains both. --- security/cert/auth.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/security/cert/auth.go b/security/cert/auth.go index 11a28664..b2fa375f 100644 --- a/security/cert/auth.go +++ b/security/cert/auth.go @@ -212,7 +212,7 @@ func (c *Authority) VerifyAnyQC(proposal *hotstuff.ProposeMsg) error { // VerifySyncInfo verifies the sync info and returns the highest QC found (if any), // the highest view, whether it was a timeout, and an error if verification failed. -func (c *Authority) VerifySyncInfo(syncInfo hotstuff.SyncInfo) (qc *hotstuff.QuorumCert, view hotstuff.View, timeout bool, err error) { +func (c *Authority) VerifySyncInfo(syncInfo hotstuff.SyncInfo) (highQC *hotstuff.QuorumCert, view hotstuff.View, timeout bool, err error) { if timeoutCert, haveTC := syncInfo.TC(); haveTC { if err := c.VerifyTimeoutCert(timeoutCert); err != nil { return nil, 0, timeout, fmt.Errorf("failed to verify timeout certificate: %w", err) @@ -222,25 +222,26 @@ func (c *Authority) VerifySyncInfo(syncInfo hotstuff.SyncInfo) (qc *hotstuff.Quo } if aggQC, haveQC := syncInfo.AggQC(); haveQC { - highQC, err := c.VerifyAggregateQC(aggQC) + qc, err := c.VerifyAggregateQC(aggQC) if err != nil { return nil, 0, timeout, fmt.Errorf("failed to verify aggregate quorum certificate: %w", err) } view = max(view, aggQC.View()) - timeout = true // timeout is true here since AggQC represents a timeout - return &highQC, view, timeout, nil + timeout = true // an aggregate QC represents a timeout + highQC = &qc + } - } else if qc, haveQC := syncInfo.QC(); haveQC { + if qc, haveQC := syncInfo.QC(); haveQC { if err := c.VerifyQuorumCert(qc); err != nil { return nil, 0, timeout, fmt.Errorf("failed to verify quorum certificate: %w", err) } - // use the QC's view if greater or equal to the TC's view; in which case, it's not a timeout. - if qc.View() >= view { - view = qc.View() + view = max(view, qc.View()) + if qc.View() == view { + // QC's view is highest or equal; it's not a timeout + highQC = &qc timeout = false } - return &qc, view, timeout, nil } - return nil, view, timeout, nil // no high QC available + return highQC, view, timeout, nil } From 588bef8a37d71b6ae033e206def7c2ad927b4291 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Sun, 16 Nov 2025 17:40:02 +0100 Subject: [PATCH 5/5] fix: VerifySyncInfo to keep QC as highQC if AggQC didn't provide one Even though the QC has a lower view than the timeout, we still keep it as highQC, that is, no AggQC provided a highQC. --- security/cert/auth.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/security/cert/auth.go b/security/cert/auth.go index b2fa375f..ce37c4aa 100644 --- a/security/cert/auth.go +++ b/security/cert/auth.go @@ -221,7 +221,7 @@ func (c *Authority) VerifySyncInfo(syncInfo hotstuff.SyncInfo) (highQC *hotstuff timeout = true } - if aggQC, haveQC := syncInfo.AggQC(); haveQC { + if aggQC, haveAggQC := syncInfo.AggQC(); haveAggQC { qc, err := c.VerifyAggregateQC(aggQC) if err != nil { return nil, 0, timeout, fmt.Errorf("failed to verify aggregate quorum certificate: %w", err) @@ -237,9 +237,12 @@ func (c *Authority) VerifySyncInfo(syncInfo hotstuff.SyncInfo) (highQC *hotstuff } view = max(view, qc.View()) if qc.View() == view { - // QC's view is highest or equal; it's not a timeout + // QC's view is higher or equal: not a timeout highQC = &qc timeout = false + } else if highQC == nil { + // QC's view is lower, but there was no AggQC + highQC = &qc } }