github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/voteaggregator/vote_aggregator.go (about) 1 package voteaggregator 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "time" 8 9 "github.com/rs/zerolog" 10 11 "github.com/onflow/flow-go/consensus/hotstuff" 12 "github.com/onflow/flow-go/consensus/hotstuff/model" 13 "github.com/onflow/flow-go/engine" 14 "github.com/onflow/flow-go/engine/common/fifoqueue" 15 "github.com/onflow/flow-go/module" 16 "github.com/onflow/flow-go/module/component" 17 "github.com/onflow/flow-go/module/counters" 18 "github.com/onflow/flow-go/module/irrecoverable" 19 "github.com/onflow/flow-go/module/mempool" 20 "github.com/onflow/flow-go/module/metrics" 21 ) 22 23 // defaultVoteAggregatorWorkers number of workers to dispatch events for vote aggregators 24 const defaultVoteAggregatorWorkers = 8 25 26 // defaultVoteQueueCapacity maximum capacity of buffering unprocessed votes 27 const defaultVoteQueueCapacity = 1000 28 29 // defaultBlockQueueCapacity maximum capacity of buffering unprocessed blocks 30 const defaultBlockQueueCapacity = 1000 31 32 // VoteAggregator stores the votes and aggregates them into a QC when enough votes have been collected 33 // VoteAggregator is designed in a way that it can aggregate votes for collection & consensus clusters 34 // that is why implementation relies on dependency injection. 35 type VoteAggregator struct { 36 *component.ComponentManager 37 log zerolog.Logger 38 hotstuffMetrics module.HotstuffMetrics 39 engineMetrics module.EngineMetrics 40 notifier hotstuff.VoteAggregationViolationConsumer 41 lowestRetainedView counters.StrictMonotonousCounter // lowest view, for which we still process votes 42 collectors hotstuff.VoteCollectors 43 queuedMessagesNotifier engine.Notifier 44 finalizationEventsNotifier engine.Notifier 45 finalizedView counters.StrictMonotonousCounter // cache the last finalized view to queue up the pruning work, and unblock the caller who's delivering the finalization event. 46 queuedVotes *fifoqueue.FifoQueue 47 queuedBlocks *fifoqueue.FifoQueue 48 } 49 50 var _ hotstuff.VoteAggregator = (*VoteAggregator)(nil) 51 var _ component.Component = (*VoteAggregator)(nil) 52 53 // NewVoteAggregator creates an instance of vote aggregator 54 // Note: verifyingProcessorFactory is injected. Thereby, the code is agnostic to the 55 // different voting formats of main Consensus vs Collector consensus. 56 func NewVoteAggregator( 57 log zerolog.Logger, 58 hotstuffMetrics module.HotstuffMetrics, 59 engineMetrics module.EngineMetrics, 60 mempoolMetrics module.MempoolMetrics, 61 notifier hotstuff.VoteAggregationViolationConsumer, 62 lowestRetainedView uint64, 63 collectors hotstuff.VoteCollectors, 64 ) (*VoteAggregator, error) { 65 66 queuedVotes, err := fifoqueue.NewFifoQueue(defaultVoteQueueCapacity, 67 fifoqueue.WithLengthObserver(func(len int) { mempoolMetrics.MempoolEntries(metrics.ResourceBlockVoteQueue, uint(len)) })) 68 if err != nil { 69 return nil, fmt.Errorf("could not initialize votes queue") 70 } 71 72 queuedBlocks, err := fifoqueue.NewFifoQueue(defaultBlockQueueCapacity) // TODO metrics 73 if err != nil { 74 return nil, fmt.Errorf("could not initialize blocks queue") 75 } 76 77 aggregator := &VoteAggregator{ 78 log: log.With().Str("component", "hotstuff.vote_aggregator").Logger(), 79 hotstuffMetrics: hotstuffMetrics, 80 engineMetrics: engineMetrics, 81 notifier: notifier, 82 lowestRetainedView: counters.NewMonotonousCounter(lowestRetainedView), 83 finalizedView: counters.NewMonotonousCounter(lowestRetainedView), 84 collectors: collectors, 85 queuedVotes: queuedVotes, 86 queuedBlocks: queuedBlocks, 87 queuedMessagesNotifier: engine.NewNotifier(), 88 finalizationEventsNotifier: engine.NewNotifier(), 89 } 90 91 // manager for own worker routines plus the internal collectors 92 componentBuilder := component.NewComponentManagerBuilder() 93 var wg sync.WaitGroup 94 wg.Add(defaultVoteAggregatorWorkers) 95 for i := 0; i < defaultVoteAggregatorWorkers; i++ { // manager for worker routines that process inbound messages 96 componentBuilder.AddWorker(func(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) { 97 defer wg.Done() 98 ready() 99 aggregator.queuedMessagesProcessingLoop(ctx) 100 }) 101 } 102 componentBuilder.AddWorker(func(_ irrecoverable.SignalerContext, ready component.ReadyFunc) { 103 // create new context which is not connected to parent 104 // we need to ensure that our internal workers stop before asking 105 // vote collectors to stop. We want to avoid delivering events to already stopped vote collectors 106 ctx, cancel := context.WithCancel(context.Background()) 107 signalerCtx, _ := irrecoverable.WithSignaler(ctx) 108 // start vote collectors 109 collectors.Start(signalerCtx) 110 <-collectors.Ready() 111 112 ready() 113 114 // wait for internal workers to stop 115 wg.Wait() 116 // signal vote collectors to stop 117 cancel() 118 // wait for it to stop 119 <-collectors.Done() 120 }) 121 componentBuilder.AddWorker(func(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) { 122 ready() 123 aggregator.finalizationProcessingLoop(ctx) 124 }) 125 126 aggregator.ComponentManager = componentBuilder.Build() 127 return aggregator, nil 128 } 129 130 func (va *VoteAggregator) queuedMessagesProcessingLoop(ctx irrecoverable.SignalerContext) { 131 notifier := va.queuedMessagesNotifier.Channel() 132 for { 133 select { 134 case <-ctx.Done(): 135 return 136 case <-notifier: 137 err := va.processQueuedMessages(ctx) 138 if err != nil { 139 ctx.Throw(fmt.Errorf("internal error processing queued messages: %w", err)) 140 return 141 } 142 } 143 } 144 } 145 146 // processQueuedMessages is a function which dispatches previously queued messages on worker thread 147 // This function is called whenever we have queued messages ready to be dispatched. 148 // No errors are expected during normal operations. 149 func (va *VoteAggregator) processQueuedMessages(ctx context.Context) error { 150 for { 151 select { 152 case <-ctx.Done(): 153 return nil 154 default: 155 } 156 157 msg, ok := va.queuedBlocks.Pop() 158 if ok { 159 block := msg.(*model.Proposal) 160 err := va.processQueuedBlock(block) 161 if err != nil { 162 return fmt.Errorf("could not process pending block %v: %w", block.Block.BlockID, err) 163 } 164 165 continue 166 } 167 168 msg, ok = va.queuedVotes.Pop() 169 if ok { 170 vote := msg.(*model.Vote) 171 startTime := time.Now() 172 err := va.processQueuedVote(vote) 173 // report duration of processing one vote 174 va.hotstuffMetrics.VoteProcessingDuration(time.Since(startTime)) 175 va.engineMetrics.MessageHandled(metrics.EngineVoteAggregator, metrics.MessageBlockVote) 176 177 if err != nil { 178 return fmt.Errorf("could not process pending vote %v for block %v: %w", vote.ID(), vote.BlockID, err) 179 } 180 181 continue 182 } 183 184 // when there is no more messages in the queue, back to the loop to wait 185 // for the next incoming message to arrive. 186 return nil 187 } 188 } 189 190 // processQueuedVote performs actual processing of queued votes, this method is called from multiple 191 // concurrent goroutines. 192 func (va *VoteAggregator) processQueuedVote(vote *model.Vote) error { 193 collector, created, err := va.collectors.GetOrCreateCollector(vote.View) 194 if err != nil { 195 // ignore if our routine is outdated and some other one has pruned collectors 196 if mempool.IsBelowPrunedThresholdError(err) { 197 return nil 198 } 199 return fmt.Errorf("could not get collector for view %d: %w", 200 vote.View, err) 201 } 202 if created { 203 va.log.Info().Uint64("view", vote.View).Msg("vote collector is created by processing vote") 204 } 205 206 err = collector.AddVote(vote) 207 if err != nil { 208 return fmt.Errorf("could not process vote for view %d, blockID %v: %w", 209 vote.View, vote.BlockID, err) 210 } 211 212 va.log.Info(). 213 Uint64("view", vote.View). 214 Hex("block_id", vote.BlockID[:]). 215 Str("vote_id", vote.ID().String()). 216 Msg("vote has been processed successfully") 217 218 return nil 219 } 220 221 // processQueuedBlock performs actual processing of queued block proposals, this method is called from multiple 222 // concurrent goroutines. 223 // CAUTION: we expect that the input block's validity has been confirmed prior to calling AddBlock, 224 // including the proposer's signature. Otherwise, VoteAggregator might crash or exhibit undefined 225 // behaviour. 226 // No errors are expected during normal operation. 227 func (va *VoteAggregator) processQueuedBlock(block *model.Proposal) error { 228 // check if the block is for a view that has already been pruned (and is thus stale) 229 if block.Block.View < va.lowestRetainedView.Value() { 230 return nil 231 } 232 233 collector, created, err := va.collectors.GetOrCreateCollector(block.Block.View) 234 if err != nil { 235 if mempool.IsBelowPrunedThresholdError(err) { 236 return nil 237 } 238 return fmt.Errorf("could not get or create collector for block %v: %w", block.Block.BlockID, err) 239 } 240 if created { 241 va.log.Info(). 242 Uint64("view", block.Block.View). 243 Hex("block_id", block.Block.BlockID[:]). 244 Msg("vote collector is created by processing block") 245 } 246 247 err = collector.ProcessBlock(block) 248 if err != nil { 249 if model.IsInvalidProposalError(err) { 250 // We are attempting process a block which is invalid 251 // This should never happen, because any component that feeds blocks into VoteAggregator 252 // needs to make sure that it's submitting for processing ONLY valid blocks. 253 return fmt.Errorf("received invalid block for processing %v at view %d", block.Block.BlockID, block.Block.View) 254 } 255 return fmt.Errorf("could not process block: %v, %w", block.Block.BlockID, err) 256 } 257 258 va.log.Info(). 259 Uint64("view", block.Block.View). 260 Hex("block_id", block.Block.BlockID[:]). 261 Msg("block has been processed successfully") 262 263 return nil 264 } 265 266 // AddVote checks if vote is stale and appends vote into processing queue 267 // actual vote processing will be called in other dispatching goroutine. 268 func (va *VoteAggregator) AddVote(vote *model.Vote) { 269 log := va.log.With().Uint64("block_view", vote.View). 270 Hex("block_id", vote.BlockID[:]). 271 Hex("voter", vote.SignerID[:]). 272 Str("vote_id", vote.ID().String()).Logger() 273 // drop stale votes 274 if vote.View < va.lowestRetainedView.Value() { 275 log.Debug().Msg("drop stale votes") 276 va.engineMetrics.InboundMessageDropped(metrics.EngineVoteAggregator, metrics.MessageBlockVote) 277 return 278 } 279 280 // It's ok to silently drop votes in case our processing pipeline is full. 281 // It means that we are probably catching up. 282 if ok := va.queuedVotes.Push(vote); ok { 283 va.queuedMessagesNotifier.Notify() 284 } else { 285 log.Info().Msg("no queue capacity, dropping vote") 286 va.engineMetrics.InboundMessageDropped(metrics.EngineVoteAggregator, metrics.MessageBlockVote) 287 } 288 } 289 290 // AddBlock notifies the VoteAggregator that it should start processing votes for the given block. 291 // The input block is queued internally within the `VoteAggregator` and processed _asynchronously_ 292 // by the VoteAggregator's internal worker routines. 293 // CAUTION: we expect that the input block's validity has been confirmed prior to calling AddBlock, 294 // including the proposer's signature. Otherwise, VoteAggregator might crash or exhibit undefined 295 // behaviour. 296 func (va *VoteAggregator) AddBlock(block *model.Proposal) { 297 // It's ok to silently drop blocks in case our processing pipeline is full. 298 // It means that we are probably catching up. 299 if ok := va.queuedBlocks.Push(block); ok { 300 va.queuedMessagesNotifier.Notify() 301 } else { 302 va.log.Debug().Msgf("dropping block %x because queue is full", block.Block.BlockID) 303 } 304 } 305 306 // InvalidBlock notifies the VoteAggregator about an invalid proposal, so that it 307 // can process votes for the invalid block and slash the voters. 308 // No errors are expected during normal operations 309 func (va *VoteAggregator) InvalidBlock(proposal *model.Proposal) error { 310 slashingVoteConsumer := func(vote *model.Vote) { 311 if proposal.Block.BlockID == vote.BlockID { 312 va.notifier.OnVoteForInvalidBlockDetected(vote, proposal) 313 } 314 } 315 316 block := proposal.Block 317 collector, _, err := va.collectors.GetOrCreateCollector(block.View) 318 if err != nil { 319 // ignore if our routine is outdated and some other one has pruned collectors 320 if mempool.IsBelowPrunedThresholdError(err) { 321 return nil 322 } 323 return fmt.Errorf("could not retrieve vote collector for view %d: %w", block.View, err) 324 } 325 // registering vote consumer will deliver all previously cached votes in strict order 326 // and will keep delivering votes if more are collected 327 collector.RegisterVoteConsumer(slashingVoteConsumer) 328 return nil 329 } 330 331 // PruneUpToView deletes all votes _below_ to the given view, as well as 332 // related indices. We only retain and process whose view is equal or larger 333 // than `lowestRetainedView`. If `lowestRetainedView` is smaller than the 334 // previous value, the previous value is kept and the method call is a NoOp. 335 func (va *VoteAggregator) PruneUpToView(lowestRetainedView uint64) { 336 if va.lowestRetainedView.Set(lowestRetainedView) { 337 va.collectors.PruneUpToView(lowestRetainedView) 338 } 339 } 340 341 // OnFinalizedBlock implements the `OnFinalizedBlock` callback from the `hotstuff.FinalizationConsumer` 342 // It informs sealing.Core about finalization of respective block. 343 // 344 // CAUTION: the input to this callback is treated as trusted; precautions should be taken that messages 345 // from external nodes cannot be considered as inputs to this function 346 func (va *VoteAggregator) OnFinalizedBlock(block *model.Block) { 347 if va.finalizedView.Set(block.View) { 348 va.finalizationEventsNotifier.Notify() 349 } 350 } 351 352 // finalizationProcessingLoop is a separate goroutine that performs processing of finalization events 353 func (va *VoteAggregator) finalizationProcessingLoop(ctx context.Context) { 354 finalizationNotifier := va.finalizationEventsNotifier.Channel() 355 for { 356 select { 357 case <-ctx.Done(): 358 return 359 case <-finalizationNotifier: 360 va.PruneUpToView(va.finalizedView.Value()) 361 } 362 } 363 }