github.com/koko1123/flow-go-1@v0.29.6/consensus/hotstuff/voteaggregator/vote_aggregator.go (about) 1 package voteaggregator 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 8 "github.com/rs/zerolog" 9 10 "github.com/koko1123/flow-go-1/consensus/hotstuff" 11 "github.com/koko1123/flow-go-1/consensus/hotstuff/model" 12 "github.com/koko1123/flow-go-1/engine" 13 "github.com/koko1123/flow-go-1/engine/common/fifoqueue" 14 "github.com/koko1123/flow-go-1/engine/consensus/sealing/counters" 15 "github.com/koko1123/flow-go-1/module/component" 16 "github.com/koko1123/flow-go-1/module/irrecoverable" 17 "github.com/koko1123/flow-go-1/module/mempool" 18 ) 19 20 // defaultVoteAggregatorWorkers number of workers to dispatch events for vote aggregators 21 const defaultVoteAggregatorWorkers = 8 22 23 // defaultVoteQueueCapacity maximum capacity of buffering unprocessed votes 24 const defaultVoteQueueCapacity = 1000 25 26 // VoteAggregator stores the votes and aggregates them into a QC when enough votes have been collected 27 // VoteAggregator is designed in a way that it can aggregate votes for collection & consensus clusters 28 // that is why implementation relies on dependency injection. 29 type VoteAggregator struct { 30 *component.ComponentManager 31 log zerolog.Logger 32 notifier hotstuff.Consumer 33 lowestRetainedView counters.StrictMonotonousCounter // lowest view, for which we still process votes 34 collectors hotstuff.VoteCollectors 35 queuedVotesNotifier engine.Notifier 36 finalizationEventsNotifier engine.Notifier 37 finalizedView counters.StrictMonotonousCounter // cache the last finalized view to queue up the pruning work, and unblock the caller who's delivering the finalization event. 38 queuedVotes *fifoqueue.FifoQueue 39 } 40 41 var _ hotstuff.VoteAggregator = (*VoteAggregator)(nil) 42 var _ component.Component = (*VoteAggregator)(nil) 43 44 // NewVoteAggregator creates an instance of vote aggregator 45 // Note: verifyingProcessorFactory is injected. Thereby, the code is agnostic to the 46 // different voting formats of main Consensus vs Collector consensus. 47 func NewVoteAggregator( 48 log zerolog.Logger, 49 notifier hotstuff.Consumer, 50 lowestRetainedView uint64, 51 collectors hotstuff.VoteCollectors, 52 ) (*VoteAggregator, error) { 53 54 queuedVotes, err := fifoqueue.NewFifoQueue(defaultVoteQueueCapacity) 55 if err != nil { 56 return nil, fmt.Errorf("could not initialize votes queue") 57 } 58 59 aggregator := &VoteAggregator{ 60 log: log, 61 notifier: notifier, 62 lowestRetainedView: counters.NewMonotonousCounter(lowestRetainedView), 63 finalizedView: counters.NewMonotonousCounter(lowestRetainedView), 64 collectors: collectors, 65 queuedVotes: queuedVotes, 66 queuedVotesNotifier: engine.NewNotifier(), 67 finalizationEventsNotifier: engine.NewNotifier(), 68 } 69 70 // manager for own worker routines plus the internal collectors 71 componentBuilder := component.NewComponentManagerBuilder() 72 var wg sync.WaitGroup 73 wg.Add(defaultVoteAggregatorWorkers) 74 for i := 0; i < defaultVoteAggregatorWorkers; i++ { // manager for worker routines that process inbound votes 75 componentBuilder.AddWorker(func(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) { 76 ready() 77 aggregator.queuedVotesProcessingLoop(ctx) 78 wg.Done() 79 }) 80 } 81 componentBuilder.AddWorker(func(_ irrecoverable.SignalerContext, ready component.ReadyFunc) { 82 // create new context which is not connected to parent 83 // we need to ensure that our internal workers stop before asking 84 // vote collectors to stop. We want to avoid delivering events to already stopped vote collectors 85 ctx, cancel := context.WithCancel(context.Background()) 86 signalerCtx, _ := irrecoverable.WithSignaler(ctx) 87 // start vote collectors 88 collectors.Start(signalerCtx) 89 <-collectors.Ready() 90 91 ready() 92 93 // wait for internal workers to stop 94 wg.Wait() 95 // signal vote collectors to stop 96 cancel() 97 // wait for it to stop 98 <-collectors.Done() 99 }) 100 componentBuilder.AddWorker(func(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) { 101 ready() 102 aggregator.finalizationProcessingLoop(ctx) 103 }) 104 105 aggregator.ComponentManager = componentBuilder.Build() 106 return aggregator, nil 107 } 108 109 func (va *VoteAggregator) queuedVotesProcessingLoop(ctx irrecoverable.SignalerContext) { 110 notifier := va.queuedVotesNotifier.Channel() 111 for { 112 select { 113 case <-ctx.Done(): 114 return 115 case <-notifier: 116 err := va.processQueuedVoteEvents(ctx) 117 if err != nil { 118 ctx.Throw(fmt.Errorf("internal error processing queued vote events: %w", err)) 119 return 120 } 121 } 122 } 123 } 124 125 func (va *VoteAggregator) processQueuedVoteEvents(ctx context.Context) error { 126 for { 127 select { 128 case <-ctx.Done(): 129 return nil 130 default: 131 } 132 133 msg, ok := va.queuedVotes.Pop() 134 if ok { 135 vote := msg.(*model.Vote) 136 err := va.processQueuedVote(vote) 137 if err != nil { 138 return fmt.Errorf("could not process pending vote %v: %w", vote.ID(), err) 139 } 140 141 va.log.Info(). 142 Uint64("view", vote.View). 143 Hex("block_id", vote.BlockID[:]). 144 Str("vote_id", vote.ID().String()). 145 Msg("vote has been processed successfully") 146 147 continue 148 } 149 150 // when there is no more messages in the queue, back to the loop to wait 151 // for the next incoming message to arrive. 152 return nil 153 } 154 } 155 156 // processQueuedVote performs actual processing of queued votes, this method is called from multiple 157 // concurrent goroutines. 158 func (va *VoteAggregator) processQueuedVote(vote *model.Vote) error { 159 collector, created, err := va.collectors.GetOrCreateCollector(vote.View) 160 if created { 161 va.log.Info().Uint64("view", vote.View).Msg("vote collector is created by processing vote") 162 } 163 164 if err != nil { 165 // ignore if our routine is outdated and some other one has pruned collectors 166 if mempool.IsDecreasingPruningHeightError(err) { 167 return nil 168 } 169 return fmt.Errorf("could not get collector for view %d: %w", 170 vote.View, err) 171 } 172 err = collector.AddVote(vote) 173 if err != nil { 174 if model.IsDoubleVoteError(err) { 175 doubleVoteErr := err.(model.DoubleVoteError) 176 va.notifier.OnDoubleVotingDetected(doubleVoteErr.FirstVote, doubleVoteErr.ConflictingVote) 177 return nil 178 } 179 180 return fmt.Errorf("could not process vote for view %d, blockID %v: %w", 181 vote.View, vote.BlockID, err) 182 } 183 184 return nil 185 } 186 187 // AddVote checks if vote is stale and appends vote into processing queue 188 // actual vote processing will be called in other dispatching goroutine. 189 func (va *VoteAggregator) AddVote(vote *model.Vote) { 190 // drop stale votes 191 if vote.View < va.lowestRetainedView.Value() { 192 193 va.log.Info(). 194 Uint64("block_view", vote.View). 195 Hex("block_id", vote.BlockID[:]). 196 Hex("voter", vote.SignerID[:]). 197 Str("vote_id", vote.ID().String()). 198 Msg("drop stale votes") 199 200 return 201 } 202 203 // It's ok to silently drop votes in case our processing pipeline is full. 204 // It means that we are probably catching up. 205 if ok := va.queuedVotes.Push(vote); ok { 206 va.queuedVotesNotifier.Notify() 207 } 208 } 209 210 // AddBlock notifies the VoteAggregator about a known block so that it can start processing 211 // pending votes whose block was unknown. 212 // It also verifies the proposer vote of a block, and return whether the proposer signature is valid. 213 // Expected error returns during normal operations: 214 // * model.InvalidBlockError if the proposer's vote for its own block is invalid 215 // * mempool.DecreasingPruningHeightError if the block's view has already been pruned 216 func (va *VoteAggregator) AddBlock(block *model.Proposal) error { 217 // check if the block is for a view that has already been pruned (and is thus stale) 218 if block.Block.View < va.lowestRetainedView.Value() { 219 return mempool.NewDecreasingPruningHeightErrorf("block proposal for view %d is stale, lowestRetainedView is %d", block.Block.View, va.lowestRetainedView.Value()) 220 } 221 222 collector, created, err := va.collectors.GetOrCreateCollector(block.Block.View) 223 if err != nil { 224 return fmt.Errorf("could not get or create collector for block %v: %w", block.Block.BlockID, err) 225 } 226 227 if created { 228 va.log.Info(). 229 Uint64("view", block.Block.View). 230 Hex("block_id", block.Block.BlockID[:]). 231 Msg("vote collector is created by processing block") 232 } 233 234 err = collector.ProcessBlock(block) 235 if err != nil { 236 return fmt.Errorf("could not process block: %v, %w", block.Block.BlockID, err) 237 } 238 239 return nil 240 } 241 242 // InvalidBlock notifies the VoteAggregator about an invalid proposal, so that it 243 // can process votes for the invalid block and slash the voters. Expected error 244 // returns during normal operations: 245 // * mempool.DecreasingPruningHeightError if proposal's view has already been pruned 246 func (va *VoteAggregator) InvalidBlock(proposal *model.Proposal) error { 247 slashingVoteConsumer := func(vote *model.Vote) { 248 if proposal.Block.BlockID == vote.BlockID { 249 va.notifier.OnVoteForInvalidBlockDetected(vote, proposal) 250 } 251 } 252 253 block := proposal.Block 254 collector, _, err := va.collectors.GetOrCreateCollector(block.View) 255 if err != nil { 256 // ignore if our routine is outdated and some other one has pruned collectors 257 if mempool.IsDecreasingPruningHeightError(err) { 258 return nil 259 } 260 return fmt.Errorf("could not retrieve vote collector for view %d: %w", block.View, err) 261 } 262 // registering vote consumer will deliver all previously cached votes in strict order 263 // and will keep delivering votes if more are collected 264 collector.RegisterVoteConsumer(slashingVoteConsumer) 265 return nil 266 } 267 268 // PruneUpToView deletes all votes _below_ to the given view, as well as 269 // related indices. We only retain and process whose view is equal or larger 270 // than `lowestRetainedView`. If `lowestRetainedView` is smaller than the 271 // previous value, the previous value is kept and the method call is a NoOp. 272 func (va *VoteAggregator) PruneUpToView(lowestRetainedView uint64) { 273 if va.lowestRetainedView.Set(lowestRetainedView) { 274 va.collectors.PruneUpToView(lowestRetainedView) 275 } 276 } 277 278 // OnFinalizedBlock implements the `OnFinalizedBlock` callback from the `hotstuff.FinalizationConsumer` 279 // 280 // (1) Informs sealing.Core about finalization of respective block. 281 // 282 // CAUTION: the input to this callback is treated as trusted; precautions should be taken that messages 283 // from external nodes cannot be considered as inputs to this function 284 func (va *VoteAggregator) OnFinalizedBlock(block *model.Block) { 285 if va.finalizedView.Set(block.View) { 286 va.finalizationEventsNotifier.Notify() 287 } 288 } 289 290 // finalizationProcessingLoop is a separate goroutine that performs processing of finalization events 291 func (va *VoteAggregator) finalizationProcessingLoop(ctx context.Context) { 292 finalizationNotifier := va.finalizationEventsNotifier.Channel() 293 for { 294 select { 295 case <-ctx.Done(): 296 return 297 case <-finalizationNotifier: 298 va.PruneUpToView(va.finalizedView.Value()) 299 } 300 } 301 }