github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/timeoutcollector/timeout_collector.go (about)

     1  package timeoutcollector
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  
     7  	"github.com/rs/zerolog"
     8  
     9  	"github.com/onflow/flow-go/consensus/hotstuff"
    10  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    11  	"github.com/onflow/flow-go/module/counters"
    12  )
    13  
    14  // TimeoutCollector implements logic for collecting timeout objects. Performs deduplication, caching and processing
    15  // of timeouts, delegating those tasks to underlying modules. Emits notifications about verified QCs and TCs, if
    16  // their view is newer than any QC or TC previously known to the TimeoutCollector.
    17  // This module is safe to use in concurrent environment.
    18  type TimeoutCollector struct {
    19  	log              zerolog.Logger
    20  	timeoutsCache    *TimeoutObjectsCache // cache for tracking double timeout and timeout equivocation
    21  	notifier         hotstuff.TimeoutAggregationConsumer
    22  	processor        hotstuff.TimeoutProcessor
    23  	newestReportedQC counters.StrictMonotonousCounter // view of newest QC that was reported
    24  	newestReportedTC counters.StrictMonotonousCounter // view of newest TC that was reported
    25  }
    26  
    27  var _ hotstuff.TimeoutCollector = (*TimeoutCollector)(nil)
    28  
    29  // NewTimeoutCollector creates new instance of TimeoutCollector
    30  func NewTimeoutCollector(log zerolog.Logger,
    31  	view uint64,
    32  	notifier hotstuff.TimeoutAggregationConsumer,
    33  	processor hotstuff.TimeoutProcessor,
    34  ) *TimeoutCollector {
    35  	return &TimeoutCollector{
    36  		log: log.With().
    37  			Str("component", "hotstuff.timeout_collector").
    38  			Uint64("view", view).
    39  			Logger(),
    40  		notifier:         notifier,
    41  		timeoutsCache:    NewTimeoutObjectsCache(view),
    42  		processor:        processor,
    43  		newestReportedQC: counters.NewMonotonousCounter(0),
    44  		newestReportedTC: counters.NewMonotonousCounter(0),
    45  	}
    46  }
    47  
    48  // AddTimeout adds a Timeout Object [TO] to the collector.
    49  // When TOs from strictly more than 1/3 of consensus participants (measured by weight)
    50  // were collected, the callback for partial TC will be triggered.
    51  // After collecting TOs from a supermajority, a TC will be created and passed to the EventLoop.
    52  // Expected error returns during normal operations:
    53  //   - timeoutcollector.ErrTimeoutForIncompatibleView - submitted timeout for incompatible view
    54  //
    55  // All other exceptions are symptoms of potential state corruption.
    56  func (c *TimeoutCollector) AddTimeout(timeout *model.TimeoutObject) error {
    57  	// cache timeout
    58  	err := c.timeoutsCache.AddTimeoutObject(timeout)
    59  	if err != nil {
    60  		if errors.Is(err, ErrRepeatedTimeout) {
    61  			return nil
    62  		}
    63  		if doubleTimeoutErr, isDoubleTimeoutErr := model.AsDoubleTimeoutError(err); isDoubleTimeoutErr {
    64  			c.notifier.OnDoubleTimeoutDetected(doubleTimeoutErr.FirstTimeout, doubleTimeoutErr.ConflictingTimeout)
    65  			return nil
    66  		}
    67  		return fmt.Errorf("internal error adding timeout %v to cache for view: %d: %w", timeout.ID(), timeout.View, err)
    68  	}
    69  
    70  	err = c.processTimeout(timeout)
    71  	if err != nil {
    72  		return fmt.Errorf("internal error processing TO %v for view: %d: %w", timeout.ID(), timeout.View, err)
    73  	}
    74  	return nil
    75  }
    76  
    77  // processTimeout delegates TO processing to TimeoutProcessor, handles sentinel errors
    78  // expected errors are handled and reported to notifier. Notifies listeners about validates
    79  // QCs and TCs.
    80  // No errors are expected during normal flow of operations.
    81  func (c *TimeoutCollector) processTimeout(timeout *model.TimeoutObject) error {
    82  	err := c.processor.Process(timeout)
    83  	if err != nil {
    84  		if invalidTimeoutErr, ok := model.AsInvalidTimeoutError(err); ok {
    85  			c.notifier.OnInvalidTimeoutDetected(*invalidTimeoutErr)
    86  			return nil
    87  		}
    88  		return fmt.Errorf("internal error while processing timeout: %w", err)
    89  	}
    90  
    91  	// TODO: consider moving OnTimeoutProcessed to TimeoutAggregationConsumer, need to fix telemetry for this.
    92  	c.notifier.OnTimeoutProcessed(timeout)
    93  
    94  	// In the following, we emit notifications about new QCs, if their view is newer than any QC previously
    95  	// known to the TimeoutCollector. Note that our implementation only provides weak ordering:
    96  	//  * Over larger time scales, the emitted events are for statistically increasing views.
    97  	//  * However, on short time scales there are _no_ monotonicity guarantees w.r.t. the views.
    98  	// Explanation:
    99  	// While only QCs with strict monotonously increasing views pass the
   100  	// `if c.newestReportedQC.Set(timeout.NewestQC.View)` statement, we emit the notification in a separate
   101  	// step. Therefore, emitting the notifications is subject to races, where on very short time-scales
   102  	// the notifications can be out of order.
   103  	// Nevertheless, we note that notifications are only created for QCs that are strictly newer than any other
   104  	// known QC at the time we check via the `if ... Set(..)` statement. Thereby, we implement the desired filtering
   105  	// behaviour, i.e. that the recipient of the notifications is not spammed by old (or repeated) QCs.
   106  	// Reasoning for this approach:
   107  	// The current implementation is completely lock-free without noteworthy risk of congestion. For the recipient
   108  	// of the notifications, the weak ordering is of no concern, because it anyway is only interested in the newest
   109  	// QC. Time-localized disorder is irrelevant, because newer QCs that would arrive later in a strongly ordered
   110  	// system can only arrive earlier in our weakly ordered implementation. Hence, if anything, the recipient
   111  	// receives the desired information _earlier_ but not later.
   112  	if c.newestReportedQC.Set(timeout.NewestQC.View) {
   113  		c.notifier.OnNewQcDiscovered(timeout.NewestQC)
   114  	}
   115  	// Same explanation for weak ordering of QCs also applies to TCs.
   116  	if timeout.LastViewTC != nil {
   117  		if c.newestReportedTC.Set(timeout.LastViewTC.View) {
   118  			c.notifier.OnNewTcDiscovered(timeout.LastViewTC)
   119  		}
   120  	}
   121  
   122  	return nil
   123  }
   124  
   125  // View returns view which is associated with this timeout collector
   126  func (c *TimeoutCollector) View() uint64 {
   127  	return c.timeoutsCache.View()
   128  }