Skip to content

Commit 7e0964b

Browse files
authored
Merge pull request #7918 from onflow/yurii/6127-vote-double-counting
[BFT] Extend `CombinedVoteProcessorV3`
2 parents e6a9436 + e7150ea commit 7e0964b

17 files changed

+934
-210
lines changed

consensus/hotstuff/model/errors.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,9 @@ func IsByzantineThresholdExceededError(err error) bool {
307307
return errors.As(err, &target)
308308
}
309309

310-
// DoubleVoteError indicates that a consensus replica has voted for two different
311-
// blocks, or has provided two semantically different votes for the same block.
310+
// DoubleVoteError indicates that a consensus replica has voted for two different blocks
311+
// in the same view, or has provided two semantically different votes for the same block.
312+
// This is a PROTOCOL VIOLATION (slashable equivocation attack).
312313
type DoubleVoteError struct {
313314
FirstVote *Vote
314315
ConflictingVote *Vote
@@ -340,6 +341,9 @@ func (e DoubleVoteError) Unwrap() error {
340341
return e.err
341342
}
342343

344+
// NewDoubleVoteErrorf creates an error signalling that a consensus replica has voted for two different
345+
// blocks in the same view, or has provided two semantically different votes for the same block.
346+
// This is a PROTOCOL VIOLATION (slashable equivocation attack).
343347
func NewDoubleVoteErrorf(firstVote, conflictingVote *Vote, msg string, args ...interface{}) error {
344348
return DoubleVoteError{
345349
FirstVote: firstVote,
@@ -370,7 +374,7 @@ func IsDuplicatedSignerError(err error) bool {
370374
return errors.As(err, &e)
371375
}
372376

373-
// InvalidSignatureIncludedError indicates that some signatures, included via TrustedAdd, are invalid
377+
// InvalidSignatureIncludedError indicates that some signatures are invalid
374378
type InvalidSignatureIncludedError struct {
375379
err error
376380
}

consensus/hotstuff/signature/randombeacon_inspector.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ import (
55

66
"github.com/onflow/crypto"
77

8+
"github.com/onflow/flow-go/consensus/hotstuff"
89
"github.com/onflow/flow-go/consensus/hotstuff/model"
910
"github.com/onflow/flow-go/module/signature"
1011
)
1112

12-
// randomBeaconInspector implements hotstuff.RandomBeaconInspector interface.
13+
// randomBeaconInspector implements [hotstuff.RandomBeaconInspector] interface.
1314
// All methods of this structure are concurrency-safe.
1415
type randomBeaconInspector struct {
1516
inspector crypto.ThresholdSignatureInspector
1617
}
1718

19+
var _ hotstuff.RandomBeaconInspector = (*randomBeaconInspector)(nil)
20+
1821
// NewRandomBeaconInspector instantiates a new randomBeaconInspector.
1922
// The constructor errors with a `model.ConfigurationError` in any of the following cases
2023
// - n is not between `ThresholdSignMinSize` and `ThresholdSignMaxSize`,
@@ -50,8 +53,8 @@ func NewRandomBeaconInspector(
5053
// execute the business logic, without interfering with each other).
5154
// It allows concurrent verification of the given signature.
5255
// Returns :
53-
// - model.InvalidSignerError if signerIndex is invalid
54-
// - model.ErrInvalidSignature if signerID is valid but signature is cryptographically invalid
56+
// - [model.InvalidSignerError] if signerIndex is invalid
57+
// - [model.ErrInvalidSignature] if signerID is valid but signature is cryptographically invalid
5558
// - other error if there is an unexpected exception.
5659
func (r *randomBeaconInspector) Verify(signerIndex int, share crypto.Signature) error {
5760
valid, err := r.inspector.VerifyShare(signerIndex, share)
@@ -75,14 +78,14 @@ func (r *randomBeaconInspector) Verify(signerIndex int, share crypto.Signature)
7578
// are returned) through a post-check (verifying the threshold signature
7679
// _after_ reconstruction before returning it).
7780
// The function is thread-safe but locks its internal state, thereby permitting only
78-
// one routine at a time to add a signature.
81+
// one routine at a time to add a signature (inherited from the underlying inspector).
7982
// Returns:
8083
// - (true, nil) if the signature has been added, and enough shares have been collected.
8184
// - (false, nil) if the signature has been added, but not enough shares were collected.
8285
//
8386
// The following errors are expected during normal operations:
84-
// - model.InvalidSignerError if signerIndex is invalid (out of the valid range)
85-
// - model.DuplicatedSignerError if the signer has been already added
87+
// - [model.InvalidSignerError] if signerIndex is invalid (out of the valid range)
88+
// - [model.DuplicatedSignerError] if the signer has been already added
8689
// - other error if there is an unexpected exception.
8790
func (r *randomBeaconInspector) TrustedAdd(signerIndex int, share crypto.Signature) (bool, error) {
8891
// Trusted add to the crypto layer
@@ -112,8 +115,8 @@ func (r *randomBeaconInspector) EnoughShares() bool {
112115
//
113116
// Returns:
114117
// - (signature, nil) if no error occurred
115-
// - (nil, model.InsufficientSignaturesError) if not enough shares were collected
116-
// - (nil, model.InvalidSignatureIncluded) if at least one collected share does not serialize to a valid BLS signature,
118+
// - (nil, [model.InsufficientSignaturesError]) if not enough shares were collected
119+
// - (nil, [model.InvalidSignatureIncluded]) if at least one collected share does not serialize to a valid BLS signature,
117120
// or if the constructed signature failed to verify against the group public key and stored message. This post-verification
118121
// is required for safety, as `TrustedAdd` allows adding invalid signatures.
119122
// - (nil, error) for any other unexpected error.

consensus/hotstuff/signature/randombeacon_reconstructor.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import (
1111
"github.com/onflow/flow-go/state/protocol"
1212
)
1313

14-
// RandomBeaconReconstructor implements hotstuff.RandomBeaconReconstructor.
15-
// The implementation wraps the hotstuff.RandomBeaconInspector and translates the signer identity into signer index.
14+
// RandomBeaconReconstructor implements [hotstuff.RandomBeaconReconstructor].
15+
// The implementation wraps the [hotstuff.RandomBeaconInspector] and translates the signer identity into signer index.
1616
// It has knowledge about DKG to be able to map signerID to signerIndex
1717
type RandomBeaconReconstructor struct {
1818
hotstuff.RandomBeaconInspector // a stateful object for this epoch. It's used for both verifying all sig shares and reconstructing the threshold signature.
@@ -33,8 +33,8 @@ func NewRandomBeaconReconstructor(dkg hotstuff.DKG, randomBeaconInspector hotstu
3333
// execute the business logic, without interfering with each other).
3434
// It allows concurrent verification of the given signature.
3535
// Returns :
36-
// - model.InvalidSignerError if signerID is invalid
37-
// - model.ErrInvalidSignature if signerID is valid but signature is cryptographically invalid
36+
// - [model.InvalidSignerError] if signerID is invalid
37+
// - [model.ErrInvalidSignature] if signerID is valid but signature is cryptographically invalid
3838
// - other error if there is an unexpected exception.
3939
func (r *RandomBeaconReconstructor) Verify(signerID flow.Identifier, sig crypto.Signature) error {
4040
signerIndex, err := r.dkg.Index(signerID)
@@ -60,8 +60,8 @@ func (r *RandomBeaconReconstructor) Verify(signerID flow.Identifier, sig crypto.
6060
// - (false, nil) if the signature has been added, but not enough shares were collected.
6161
//
6262
// The following errors are expected during normal operations:
63-
// - model.InvalidSignerError if signerIndex is invalid (out of the valid range)
64-
// - model.DuplicatedSignerError if the signer has been already added
63+
// - [model.InvalidSignerError] if signerIndex is invalid (out of the valid range)
64+
// - [model.DuplicatedSignerError] if the signer has been already added
6565
// - other error if there is an unexpected exception.
6666
func (r *RandomBeaconReconstructor) TrustedAdd(signerID flow.Identifier, sig crypto.Signature) (bool, error) {
6767
signerIndex, err := r.dkg.Index(signerID)

consensus/hotstuff/signature/weighted_signature_aggregator.go

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,24 @@ type signerInfo struct {
1919
index int
2020
}
2121

22-
// WeightedSignatureAggregator implements consensus/hotstuff.WeightedSignatureAggregator.
23-
// It is a wrapper around module/signature.SignatureAggregatorSameMessage, which implements a
22+
// WeightedSignatureAggregator implements [hotstuff.WeightedSignatureAggregator].
23+
// It is a wrapper around [signature.SignatureAggregatorSameMessage], implementing a
2424
// mapping from node IDs (as used by HotStuff) to index-based addressing of authorized
25-
// signers (as used by SignatureAggregatorSameMessage).
25+
// signers (as used by [signature.SignatureAggregatorSameMessage]).
2626
//
27-
// Similarly to module/signature.SignatureAggregatorSameMessage, this module assumes proofs of possession (PoP)
28-
// of all identity public keys are valid.
27+
// We delegate the handling of duplicate signatures to the underlying [signature.SignatureAggregatorSameMessage].
28+
// NOTE: This is possible, because [signature.SignatureAggregatorSameMessage] does not support signatures with
29+
// multiplicity higher than 1, i.e. each signer is allowed to sign at most once. Should this constraint every be
30+
// changed in [signature.SignatureAggregatorSameMessage], this module would need to be updated accordingly.
31+
//
32+
// Similarly to [signature.SignatureAggregatorSameMessage], this module assumes proofs
33+
// of possession (PoP) of all identity public keys are valid.
2934
type WeightedSignatureAggregator struct {
3035
aggregator *signature.SignatureAggregatorSameMessage // low level crypto BLS aggregator, agnostic of weights and flow IDs
3136
ids flow.IdentityList // all possible ids (only gets updated by constructor)
3237
idToInfo map[flow.Identifier]signerInfo // auxiliary map to lookup signer weight and index by ID (only gets updated by constructor)
3338
totalWeight uint64 // weight collected (gets updated)
3439
lock sync.RWMutex // lock for atomic updates to totalWeight and collectedIDs
35-
36-
// collectedIDs tracks the Identities of all nodes whose signatures have been collected so far.
37-
// The reason for tracking the duplicate signers at this module level is that having no duplicates
38-
// is a Hotstuff constraint, rather than a cryptographic aggregation constraint. We are planning to
39-
// extend the cryptographic primitives to support multiplicity higher than 1 in the future.
40-
// Therefore, we already add the logic for identifying duplicates here.
41-
collectedIDs map[flow.Identifier]struct{} // map of collected IDs (gets updated)
4240
}
4341

4442
var _ hotstuff.WeightedSignatureAggregator = (*WeightedSignatureAggregator)(nil)
@@ -81,17 +79,16 @@ func NewWeightedSignatureAggregator(
8179
}
8280

8381
return &WeightedSignatureAggregator{
84-
aggregator: agg,
85-
ids: ids,
86-
idToInfo: idToInfo,
87-
collectedIDs: make(map[flow.Identifier]struct{}),
82+
aggregator: agg,
83+
ids: ids,
84+
idToInfo: idToInfo,
8885
}, nil
8986
}
9087

9188
// Verify verifies the signature under the stored public keys and message.
9289
// Expected errors during normal operations:
93-
// - model.InvalidSignerError if signerID is invalid (not a consensus participant)
94-
// - model.ErrInvalidSignature if signerID is valid but signature is cryptographically invalid
90+
// - [model.InvalidSignerError] if signerID is invalid (not a consensus participant)
91+
// - [model.ErrInvalidSignature] if signerID is valid but signature is cryptographically invalid
9592
//
9693
// The function is thread-safe.
9794
func (w *WeightedSignatureAggregator) Verify(signerID flow.Identifier, sig crypto.Signature) error {
@@ -116,8 +113,8 @@ func (w *WeightedSignatureAggregator) Verify(signerID flow.Identifier, sig crypt
116113
// The total weight of all collected signatures (excluding duplicates) is returned regardless
117114
// of any returned error.
118115
// The function errors with:
119-
// - model.InvalidSignerError if signerID is invalid (not a consensus participant)
120-
// - model.DuplicatedSignerError if the signer has been already added
116+
// - [model.InvalidSignerError] if signerID is invalid (not a consensus participant)
117+
// - [model.DuplicatedSignerError] if the signer has been already added
121118
//
122119
// The function is thread-safe.
123120
func (w *WeightedSignatureAggregator) TrustedAdd(signerID flow.Identifier, sig crypto.Signature) (uint64, error) {
@@ -130,19 +127,20 @@ func (w *WeightedSignatureAggregator) TrustedAdd(signerID flow.Identifier, sig c
130127
w.lock.Lock()
131128
defer w.lock.Unlock()
132129

133-
// check for repeated occurrence of signerID (in anticipation of aggregator supporting multiplicities larger than 1 in the future)
134-
if _, duplicate := w.collectedIDs[signerID]; duplicate {
135-
return w.totalWeight, model.NewDuplicatedSignerErrorf("signature from %v was already added", signerID)
136-
}
137-
130+
// NOTE: We delegate the handling of duplicate signatures to the underlying [signature.SignatureAggregatorSameMessage].
131+
// This is valid only because [signature.SignatureAggregatorSameMessage] does not support signatures with multiplicity
132+
// higher than 1, i.e. each signer is allowed to sign at most once. Should this constraint every be relaxed in
133+
// [signature.SignatureAggregatorSameMessage], we need to implement it here in the `WeightedSignatureAggregator`,
134+
// because in the context of HotStuff each consensus replica is allowed to vote at most once.
138135
err := w.aggregator.TrustedAdd(info.index, sig)
139136
if err != nil {
140-
// During normal operations, signature.InvalidSignerIdxError or signature.DuplicatedSignerIdxError should never occur.
137+
if signature.IsDuplicatedSignerIdxError(err) {
138+
return w.totalWeight, model.NewDuplicatedSignerErrorf("signature from %v was already added", signerID)
139+
}
140+
// During normal operations, signature.InvalidSignerIdxError should never occur.
141141
return w.totalWeight, fmt.Errorf("unexpected exception while trusted add of signature from %v: %w", signerID, err)
142142
}
143143
w.totalWeight += info.weight
144-
w.collectedIDs[signerID] = struct{}{}
145-
146144
return w.totalWeight, nil
147145
}
148146

@@ -158,12 +156,12 @@ func (w *WeightedSignatureAggregator) TotalWeight() uint64 {
158156
// The function performs a final verification and errors if the aggregated signature is invalid. This is
159157
// required for the function safety since `TrustedAdd` allows adding invalid signatures.
160158
// The function errors with:
161-
// - model.InsufficientSignaturesError if no signatures have been added yet
162-
// - model.InvalidSignatureIncludedError if:
159+
// - [model.InsufficientSignaturesError] if no signatures have been added yet
160+
// - [model.InvalidSignatureIncludedError] if:
163161
// - some signature(s), included via TrustedAdd, fail to deserialize (regardless of the aggregated public key)
164162
// -- or all signatures deserialize correctly but some signature(s), included via TrustedAdd, are
165163
// invalid (while aggregated public key is valid)
166-
// -- model.InvalidAggregatedKeyError if all signatures deserialize correctly but the signer's
164+
// -- [model.InvalidAggregatedKeyError] if all signatures deserialize correctly but the signer's
167165
// staking public keys sum up to an invalid key (BLS identity public key).
168166
// Any aggregated signature would fail the cryptographic verification under the identity public
169167
// key and therefore such signature is considered invalid. Such scenario can only happen if

consensus/hotstuff/vote_collector.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,15 @@ type VoteCollector interface {
5959
// ProcessBlock performs validation of block signature and processes block with respected collector.
6060
// Calling this function will mark conflicting collector as stale and change state of valid collectors
6161
// It returns nil if the block is valid.
62-
// It returns model.InvalidProposalError if block is invalid.
62+
// It returns [model.InvalidProposalError] if block is invalid.
6363
// It returns other error if there is exception processing the block.
6464
ProcessBlock(block *model.SignedProposal) error
6565

66-
// AddVote adds a vote to the collector
67-
// When enough votes have been added to produce a QC, the QC will be created asynchronously, and
68-
// passed to EventLoop through a callback.
69-
// No errors are expected during normal operations.
66+
// AddVote adds a vote to the vote collector. The vote must be for the `VoteCollector`'s view (otherwise,
67+
// an exception is returned). When enough votes have been added to produce a QC, the QC will be created
68+
// asynchronously, and passed to EventLoop through a callback.
69+
// All byzantine edge cases are handled internally via callbacks to notifier.
70+
// Under normal execution only exceptions are propagated to caller.
7071
AddVote(vote *model.Vote) error
7172

7273
// RegisterVoteConsumer registers a VoteConsumer. Upon registration, the collector
@@ -88,19 +89,27 @@ type VoteCollector interface {
8889
// Depending on their implementation, a VoteProcessor might drop votes or attempt to construct a QC.
8990
type VoteProcessor interface {
9091
// Process performs processing of single vote. This function is safe to call from multiple goroutines.
92+
//
9193
// Expected error returns during normal operations:
92-
// * VoteForIncompatibleBlockError - submitted vote for incompatible block
93-
// * VoteForIncompatibleViewError - submitted vote for incompatible view
94-
// * model.InvalidVoteError - submitted vote with invalid signature
95-
// * model.DuplicatedSignerError - vote from a signer whose vote was previously already processed
94+
// - [VoteForIncompatibleBlockError] if vote is for incompatible block
95+
// - [VoteForIncompatibleViewError] if vote is for incompatible view
96+
// - [model.InvalidVoteError] if vote has invalid signature
97+
// - [model.DuplicatedSignerError] if the same vote from the same signer has been already added
98+
//
9699
// All other errors should be treated as exceptions.
97100
Process(vote *model.Vote) error
98101

99102
// Status returns the status of the vote processor
100103
Status() VoteCollectorStatus
101104
}
102105

103-
// VerifyingVoteProcessor is a VoteProcessor that attempts to construct a QC for the given block.
106+
// VerifyingVoteProcessor is a VoteProcessor that attempts to construct a QC for a specific block
107+
// (provided at construction time).
108+
//
109+
// IMPORTANT: The VerifyingVoteProcessor provides the final defense against any vote-equivocation attacks
110+
// for its specific block. These attacks typically aim at multiple votes from the same node being counted
111+
// towards the supermajority threshold. The VerifyingVoteProcessor must withstand attacks by the
112+
// leader concurrently utilizing stand-alone votes and votes embedded into the proposal.
104113
type VerifyingVoteProcessor interface {
105114
VoteProcessor
106115

@@ -115,6 +124,6 @@ type VoteProcessorFactory interface {
115124
// Create instantiates a VerifyingVoteProcessor for processing votes for a specific proposal.
116125
// Caller can be sure that proposal vote was successfully verified and processed.
117126
// Expected error returns during normal operations:
118-
// * model.InvalidProposalError - proposal has invalid proposer vote
127+
// * [model.InvalidProposalError] - proposal has invalid proposer vote
119128
Create(log zerolog.Logger, proposal *model.SignedProposal) (VerifyingVoteProcessor, error)
120129
}

consensus/hotstuff/voteaggregator/vote_collectors_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"sync"
77
"testing"
8+
"time"
89

910
"github.com/gammazero/workerpool"
1011
"github.com/stretchr/testify/require"
@@ -49,7 +50,7 @@ func (s *VoteCollectorsTestSuite) SetupTest() {
4950
}
5051

5152
func (s *VoteCollectorsTestSuite) TearDownTest() {
52-
s.workerPool.StopWait()
53+
unittest.AssertReturnsBefore(s.T(), s.workerPool.StopWait, time.Second)
5354
}
5455

5556
// prepareMockedCollector prepares a mocked collector and stores it in map, later it will be used

0 commit comments

Comments
 (0)