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 }