github.com/onflow/flow-go@v0.33.17/consensus/hotstuff/votecollector/combined_vote_processor_v2.go (about) 1 package votecollector 2 3 import ( 4 "errors" 5 "fmt" 6 7 "github.com/rs/zerolog" 8 "go.uber.org/atomic" 9 10 "github.com/onflow/flow-go/consensus/hotstuff" 11 "github.com/onflow/flow-go/consensus/hotstuff/model" 12 "github.com/onflow/flow-go/consensus/hotstuff/signature" 13 "github.com/onflow/flow-go/consensus/hotstuff/verification" 14 "github.com/onflow/flow-go/crypto" 15 "github.com/onflow/flow-go/model/flow" 16 msig "github.com/onflow/flow-go/module/signature" 17 ) 18 19 /* **************** Base-Factory for CombinedVoteProcessors ***************** */ 20 21 // combinedVoteProcessorFactoryBaseV2 is a `votecollector.baseFactory` for creating 22 // CombinedVoteProcessors, holding all needed dependencies. 23 // combinedVoteProcessorFactoryBaseV2 is intended to be used for the main consensus. 24 // CAUTION: 25 // this base factory only creates the VerifyingVoteProcessor for the given block. 26 // It does _not_ check the proposer's vote for its own block, i.e. it does _not_ 27 // implement `hotstuff.VoteProcessorFactory`. This base factory should be wrapped 28 // by `votecollector.VoteProcessorFactory` which adds the logic to verify 29 // the proposer's vote (decorator pattern). 30 type combinedVoteProcessorFactoryBaseV2 struct { 31 committee hotstuff.DynamicCommittee 32 onQCCreated hotstuff.OnQCCreated 33 packer hotstuff.Packer 34 } 35 36 // Create creates CombinedVoteProcessorV2 for processing votes for the given block. 37 // Caller must treat all errors as exceptions 38 func (f *combinedVoteProcessorFactoryBaseV2) Create(log zerolog.Logger, block *model.Block) (hotstuff.VerifyingVoteProcessor, error) { 39 allParticipants, err := f.committee.IdentitiesByBlock(block.BlockID) 40 if err != nil { 41 return nil, fmt.Errorf("error retrieving consensus participants at block %v: %w", block.BlockID, err) 42 } 43 44 // message that has to be verified against aggregated signature 45 msg := verification.MakeVoteMessage(block.View, block.BlockID) 46 47 // prepare the staking public keys of participants 48 stakingKeys := make([]crypto.PublicKey, 0, len(allParticipants)) 49 for _, participant := range allParticipants { 50 stakingKeys = append(stakingKeys, participant.StakingPubKey) 51 } 52 53 stakingSigAggtor, err := signature.NewWeightedSignatureAggregator(allParticipants, stakingKeys, msg, msig.ConsensusVoteTag) 54 if err != nil { 55 return nil, fmt.Errorf("could not create aggregator for staking signatures at block %v: %w", block.BlockID, err) 56 } 57 58 publicKeyShares := make([]crypto.PublicKey, 0, len(allParticipants)) 59 dkg, err := f.committee.DKG(block.View) 60 if err != nil { 61 return nil, fmt.Errorf("could not get DKG info at block %v: %w", block.BlockID, err) 62 } 63 for _, participant := range allParticipants { 64 pk, err := dkg.KeyShare(participant.NodeID) 65 if err != nil { 66 return nil, fmt.Errorf("could not get random beacon key share for %x at block %v: %w", participant.NodeID, block.BlockID, err) 67 } 68 publicKeyShares = append(publicKeyShares, pk) 69 } 70 71 threshold := msig.RandomBeaconThreshold(int(dkg.Size())) 72 randomBeaconInspector, err := signature.NewRandomBeaconInspector(dkg.GroupKey(), publicKeyShares, threshold, msg) 73 if err != nil { 74 return nil, fmt.Errorf("could not create random beacon inspector at block %v: %w", block.BlockID, err) 75 } 76 77 rbRector := signature.NewRandomBeaconReconstructor(dkg, randomBeaconInspector) 78 minRequiredWeight, err := f.committee.QuorumThresholdForView(block.View) 79 if err != nil { 80 return nil, fmt.Errorf("could not get weight threshold for view %d: %w", block.View, err) 81 } 82 83 return NewCombinedVoteProcessor( 84 log, 85 block, 86 stakingSigAggtor, 87 rbRector, 88 f.onQCCreated, 89 f.packer, 90 minRequiredWeight, 91 ), nil 92 } 93 94 /* ****************** CombinedVoteProcessorV2 Implementation ****************** */ 95 96 // CombinedVoteProcessorV2 implements the hotstuff.VerifyingVoteProcessor interface. 97 // It processes votes from the main consensus committee, where participants must 98 // _always_ provide the staking signature as part of their vote and can _optionally_ 99 // also provide a random beacon signature. Through their staking signature, a 100 // participant always contributes to HotStuff's progress. Participation in the random 101 // beacon is optional (but encouraged). This allows nodes that failed the DKG to 102 // still contribute only to consensus (as fallback). 103 // CombinedVoteProcessorV2 is Concurrency safe. 104 type CombinedVoteProcessorV2 struct { 105 log zerolog.Logger 106 block *model.Block 107 stakingSigAggtor hotstuff.WeightedSignatureAggregator 108 rbRector hotstuff.RandomBeaconReconstructor 109 onQCCreated hotstuff.OnQCCreated 110 packer hotstuff.Packer 111 minRequiredWeight uint64 112 done atomic.Bool 113 } 114 115 var _ hotstuff.VerifyingVoteProcessor = (*CombinedVoteProcessorV2)(nil) 116 117 func NewCombinedVoteProcessor(log zerolog.Logger, 118 block *model.Block, 119 stakingSigAggtor hotstuff.WeightedSignatureAggregator, 120 rbRector hotstuff.RandomBeaconReconstructor, 121 onQCCreated hotstuff.OnQCCreated, 122 packer hotstuff.Packer, 123 minRequiredWeight uint64, 124 ) *CombinedVoteProcessorV2 { 125 return &CombinedVoteProcessorV2{ 126 log: log.With().Hex("block_id", block.BlockID[:]).Logger(), 127 block: block, 128 stakingSigAggtor: stakingSigAggtor, 129 rbRector: rbRector, 130 onQCCreated: onQCCreated, 131 packer: packer, 132 minRequiredWeight: minRequiredWeight, 133 done: *atomic.NewBool(false), 134 } 135 } 136 137 // Block returns block that is part of proposal that we are processing votes for. 138 func (p *CombinedVoteProcessorV2) Block() *model.Block { 139 return p.block 140 } 141 142 // Status returns status of this vote processor, it's always verifying. 143 func (p *CombinedVoteProcessorV2) Status() hotstuff.VoteCollectorStatus { 144 return hotstuff.VoteCollectorStatusVerifying 145 } 146 147 // Process performs processing of single vote in concurrent safe way. This function is implemented to be 148 // called by multiple goroutines at the same time. Supports processing of both staking and random beacon signatures. 149 // Design of this function is event driven: as soon as we collect enough signatures to create a QC we will immediately do so 150 // and submit it via callback for further processing. 151 // Expected error returns during normal operations: 152 // * VoteForIncompatibleBlockError - submitted vote for incompatible block 153 // * VoteForIncompatibleViewError - submitted vote for incompatible view 154 // * model.InvalidVoteError - submitted vote with invalid signature 155 // * model.DuplicatedSignerError if the signer has been already added 156 // All other errors should be treated as exceptions. 157 // 158 // Impossibility of vote double-counting: Our signature scheme requires _every_ vote to supply a 159 // staking signature. Therefore, the `stakingSigAggtor` has the set of _all_ signerIDs that have 160 // provided a valid vote. Hence, the `stakingSigAggtor` guarantees that only a single vote can 161 // be successfully added for each `signerID`, i.e. double-counting votes is impossible. 162 func (p *CombinedVoteProcessorV2) Process(vote *model.Vote) error { 163 err := EnsureVoteForBlock(vote, p.block) 164 if err != nil { 165 return fmt.Errorf("received incompatible vote %v: %w", vote.ID(), err) 166 } 167 168 // Vote Processing state machine 169 if p.done.Load() { 170 return nil 171 } 172 stakingSig, randomBeaconSig, err := msig.DecodeDoubleSig(vote.SigData) 173 if err != nil { 174 if errors.Is(err, msig.ErrInvalidSignatureFormat) { 175 return model.NewInvalidVoteErrorf(vote, "could not decode signature: %w", err) 176 } 177 return fmt.Errorf("unexpected error decoding vote %v: %w", vote.ID(), err) 178 } 179 180 // Verify staking sig. 181 err = p.stakingSigAggtor.Verify(vote.SignerID, stakingSig) 182 if err != nil { 183 if model.IsInvalidSignerError(err) { 184 return model.NewInvalidVoteErrorf(vote, "vote %x for view %d is not from an authorized consensus participant: %w", 185 vote.ID(), vote.View, err) 186 } 187 if errors.Is(err, model.ErrInvalidSignature) { 188 return model.NewInvalidVoteErrorf(vote, "vote %x for view %d has an invalid staking signature: %w", 189 vote.ID(), vote.View, err) 190 } 191 return fmt.Errorf("internal error checking signature validity for vote %v: %w", vote.ID(), err) 192 } 193 194 if p.done.Load() { 195 return nil 196 } 197 198 // Verify random beacon sig 199 if randomBeaconSig != nil { 200 err = p.rbRector.Verify(vote.SignerID, randomBeaconSig) 201 if err != nil { 202 // InvalidSignerError is possible in case we have consensus participants that are _not_ part of the random beacon committee. 203 if model.IsInvalidSignerError(err) { 204 return model.NewInvalidVoteErrorf(vote, "vote %x for view %d is not from an authorized random beacon participant: %w", 205 vote.ID(), vote.View, err) 206 } 207 if errors.Is(err, model.ErrInvalidSignature) { 208 return model.NewInvalidVoteErrorf(vote, "vote %x for view %d has an invalid random beacon signature: %w", 209 vote.ID(), vote.View, err) 210 } 211 return fmt.Errorf("internal error checking signature validity for vote %v: %w", vote.ID(), err) 212 } 213 } 214 215 if p.done.Load() { 216 return nil 217 } 218 219 // Add staking sig to aggregator. 220 _, err = p.stakingSigAggtor.TrustedAdd(vote.SignerID, stakingSig) 221 if err != nil { 222 // we don't expect any errors here during normal operation, as we previously checked 223 // for duplicated votes from the same signer and verified the signer+signature 224 return fmt.Errorf("unexpected exception adding signature from vote %v to staking aggregator: %w", vote.ID(), err) 225 } 226 // Add random beacon sig to threshold sig reconstructor 227 if randomBeaconSig != nil { 228 _, err = p.rbRector.TrustedAdd(vote.SignerID, randomBeaconSig) 229 if err != nil { 230 // we don't expect any errors here during normal operation, as we previously checked 231 // for duplicated votes from the same signer and verified the signer+signature 232 return fmt.Errorf("unexpected exception adding signature from vote %v to random beacon reconstructor: %w", vote.ID(), err) 233 } 234 } 235 236 // checking of conditions for building QC are satisfied 237 totalWeight := p.stakingSigAggtor.TotalWeight() 238 p.log.Debug().Msgf("processed vote, total weight=(%d), required=(%d)", totalWeight, p.minRequiredWeight) 239 if totalWeight < p.minRequiredWeight { 240 return nil 241 } 242 if !p.rbRector.EnoughShares() { 243 return nil 244 } 245 246 // At this point, we have enough signatures to build a QC. Another routine 247 // might just be at this point. To avoid duplicate work, only one routine can pass: 248 if !p.done.CompareAndSwap(false, true) { 249 return nil 250 } 251 252 // Our algorithm for checking votes and adding them to the aggregators should 253 // guarantee that we are _always_ able to successfully construct a QC when we 254 // reach this point. A failure implies that the VoteProcessor's internal state is corrupted. 255 qc, err := p.buildQC() 256 if err != nil { 257 return fmt.Errorf("internal error constructing QC from votes: %w", err) 258 } 259 260 p.log.Info(). 261 Uint64("view", qc.View). 262 Hex("signers", qc.SignerIndices). 263 Msg("new QC has been created") 264 265 p.onQCCreated(qc) 266 267 return nil 268 } 269 270 // buildQC performs aggregation and reconstruction of signatures when we have collected enough 271 // signatures for building a QC. This function is run only once by a single worker. 272 // Any error should be treated as exception. 273 func (p *CombinedVoteProcessorV2) buildQC() (*flow.QuorumCertificate, error) { 274 stakingSigners, aggregatedStakingSig, err := p.stakingSigAggtor.Aggregate() 275 if err != nil { 276 return nil, fmt.Errorf("could not aggregate staking signature: %w", err) 277 } 278 reconstructedBeaconSig, err := p.rbRector.Reconstruct() 279 if err != nil { 280 return nil, fmt.Errorf("could not reconstruct random beacon group signature: %w", err) 281 } 282 283 blockSigData := buildBlockSignatureDataForV2(stakingSigners, aggregatedStakingSig, reconstructedBeaconSig) 284 qc, err := buildQCWithPackerAndSigData(p.packer, p.block, blockSigData) 285 if err != nil { 286 return nil, err 287 } 288 289 return qc, nil 290 } 291 292 // buildBlockSignatureDataForV2 build a block sig data for V2 293 // It reuses the hotstuff.BlockSignatureData type to create the sig data without filling the RandomBeaconSigners field and 294 // the AggregatedRandomBeaconSig field, so that the packer can be reused by both V2 and V3 to pack QC's sig data. 295 func buildBlockSignatureDataForV2( 296 stakingSigners []flow.Identifier, 297 aggregatedStakingSig []byte, 298 reconstructedBeaconSig crypto.Signature, 299 ) *hotstuff.BlockSignatureData { 300 return &hotstuff.BlockSignatureData{ 301 StakingSigners: stakingSigners, 302 AggregatedStakingSig: aggregatedStakingSig, 303 ReconstructedRandomBeaconSig: reconstructedBeaconSig, 304 } 305 } 306 307 // buildQCWithPackerAndSigData builds the QC with the given packer and blockSigData 308 func buildQCWithPackerAndSigData( 309 packer hotstuff.Packer, 310 block *model.Block, 311 blockSigData *hotstuff.BlockSignatureData, 312 ) (*flow.QuorumCertificate, error) { 313 signerIndices, sigData, err := packer.Pack(block.View, blockSigData) 314 315 if err != nil { 316 return nil, fmt.Errorf("could not pack the block sig data: %w", err) 317 } 318 319 return &flow.QuorumCertificate{ 320 View: block.View, 321 BlockID: block.BlockID, 322 SignerIndices: signerIndices, 323 SigData: sigData, 324 }, nil 325 }