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  }