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  }