github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/epochs/epoch_lookup.go (about) 1 package epochs 2 3 import ( 4 "fmt" 5 "sync" 6 7 "go.uber.org/atomic" 8 9 "github.com/onflow/flow-go/consensus/hotstuff/model" 10 "github.com/onflow/flow-go/model/flow" 11 "github.com/onflow/flow-go/module" 12 "github.com/onflow/flow-go/module/component" 13 "github.com/onflow/flow-go/module/irrecoverable" 14 "github.com/onflow/flow-go/state/protocol" 15 "github.com/onflow/flow-go/state/protocol/events" 16 ) 17 18 // epochRange captures the counter and view range of an epoch (inclusive on both ends) 19 type epochRange struct { 20 counter uint64 21 firstView uint64 22 finalView uint64 23 } 24 25 // exists returns true when the epochRange is initialized (anything besides the zero value for the struct). 26 // It is useful for checking existence while iterating the epochRangeCache. 27 func (er epochRange) exists() bool { 28 return er != epochRange{} 29 } 30 31 // epochRangeCache stores at most the 3 latest epoch ranges. 32 // Ranges are ordered by counter (ascending) and right-aligned. 33 // For example, if we only have one epoch cached, `epochRangeCache[0]` and `epochRangeCache[1]` are `nil`. 34 // Not safe for concurrent use. 35 type epochRangeCache [3]epochRange 36 37 // latest returns the latest cached epoch range, or nil if no epochs are cached. 38 func (cache *epochRangeCache) latest() epochRange { 39 return cache[2] 40 } 41 42 // combinedRange returns the endpoints of the combined view range of all cached 43 // epochs. In particular, we return the lowest firstView and the greatest finalView. 44 // At least one epoch must already be cached, otherwise this function will panic. 45 func (cache *epochRangeCache) combinedRange() (firstView uint64, finalView uint64) { 46 47 // low end of the range is the first view of the first cached epoch 48 for _, epoch := range cache { 49 if epoch.exists() { 50 firstView = epoch.firstView 51 break 52 } 53 } 54 // high end of the range is the final view of the latest cached epoch 55 finalView = cache.latest().finalView 56 return 57 } 58 59 // add inserts an epoch range to the cache. 60 // Validates that epoch counters and view ranges are sequential. 61 // Adding the same epoch multiple times is a no-op. 62 // Guarantees ordering and alignment properties of epochRangeCache are preserved. 63 // No errors are expected during normal operation. 64 func (cache *epochRangeCache) add(epoch epochRange) error { 65 66 // sanity check: ensure the epoch we are adding is considered a non-zero value 67 // this helps ensure internal consistency in this component, but if we ever trip this check, something is seriously wrong elsewhere 68 if !epoch.exists() { 69 return fmt.Errorf("sanity check failed: caller attempted to cache invalid zero epoch") 70 } 71 72 latestCachedEpoch := cache.latest() 73 // initial case - no epoch ranges are stored yet 74 if !latestCachedEpoch.exists() { 75 cache[2] = epoch 76 return nil 77 } 78 79 // adding the same epoch multiple times is a no-op 80 if latestCachedEpoch == epoch { 81 return nil 82 } 83 84 // sanity check: ensure counters/views are sequential 85 if epoch.counter != latestCachedEpoch.counter+1 { 86 return fmt.Errorf("non-sequential epoch counters: adding epoch %d when latest cached epoch is %d", epoch.counter, latestCachedEpoch.counter) 87 } 88 if epoch.firstView != latestCachedEpoch.finalView+1 { 89 return fmt.Errorf("non-sequential epoch view ranges: adding range [%d,%d] when latest cached range is [%d,%d]", 90 epoch.firstView, epoch.finalView, latestCachedEpoch.firstView, latestCachedEpoch.finalView) 91 } 92 93 // typical case - displacing existing epoch ranges 94 // insert new epoch range, shifting existing epochs left 95 cache[0] = cache[1] // ejects oldest epoch 96 cache[1] = cache[2] 97 cache[2] = epoch 98 99 return nil 100 } 101 102 // EpochLookup implements the EpochLookup interface using protocol state to match views to epochs. 103 // CAUTION: EpochLookup should only be used for querying the previous, current, or next epoch. 104 type EpochLookup struct { 105 state protocol.State 106 mu sync.RWMutex 107 epochs epochRangeCache 108 committedEpochsCh chan *flow.Header // protocol events for newly committed epochs (the first block of the epoch is passed over the channel) 109 epochFallbackIsTriggered *atomic.Bool // true when epoch fallback is triggered 110 events.Noop // implements protocol.Consumer 111 component.Component 112 } 113 114 var _ protocol.Consumer = (*EpochLookup)(nil) 115 var _ module.EpochLookup = (*EpochLookup)(nil) 116 117 // NewEpochLookup instantiates a new EpochLookup 118 func NewEpochLookup(state protocol.State) (*EpochLookup, error) { 119 lookup := &EpochLookup{ 120 state: state, 121 committedEpochsCh: make(chan *flow.Header, 1), 122 epochFallbackIsTriggered: atomic.NewBool(false), 123 } 124 125 lookup.Component = component.NewComponentManagerBuilder(). 126 AddWorker(lookup.handleProtocolEvents). 127 Build() 128 129 final := state.Final() 130 131 // we cache the previous epoch, if one exists 132 exists, err := protocol.PreviousEpochExists(final) 133 if err != nil { 134 return nil, fmt.Errorf("could not check previous epoch exists: %w", err) 135 } 136 if exists { 137 err := lookup.cacheEpoch(final.Epochs().Previous()) 138 if err != nil { 139 return nil, fmt.Errorf("could not prepare previous epoch: %w", err) 140 } 141 } 142 143 // we always cache the current epoch 144 err = lookup.cacheEpoch(final.Epochs().Current()) 145 if err != nil { 146 return nil, fmt.Errorf("could not prepare current epoch: %w", err) 147 } 148 149 // we cache the next epoch, if it is committed 150 phase, err := final.Phase() 151 if err != nil { 152 return nil, fmt.Errorf("could not check epoch phase: %w", err) 153 } 154 if phase == flow.EpochPhaseCommitted { 155 err := lookup.cacheEpoch(final.Epochs().Next()) 156 if err != nil { 157 return nil, fmt.Errorf("could not prepare previous epoch: %w", err) 158 } 159 } 160 161 // if epoch fallback was triggered, note it here 162 triggered, err := state.Params().EpochFallbackTriggered() 163 if err != nil { 164 return nil, fmt.Errorf("could not check epoch fallback: %w", err) 165 } 166 if triggered { 167 lookup.epochFallbackIsTriggered.Store(true) 168 } 169 170 return lookup, nil 171 } 172 173 // cacheEpoch caches the given epoch's view range. Must only be called with committed epochs. 174 // No errors are expected during normal operation. 175 func (lookup *EpochLookup) cacheEpoch(epoch protocol.Epoch) error { 176 counter, err := epoch.Counter() 177 if err != nil { 178 return err 179 } 180 firstView, err := epoch.FirstView() 181 if err != nil { 182 return err 183 } 184 finalView, err := epoch.FinalView() 185 if err != nil { 186 return err 187 } 188 189 cachedEpoch := epochRange{ 190 counter: counter, 191 firstView: firstView, 192 finalView: finalView, 193 } 194 195 lookup.mu.Lock() 196 err = lookup.epochs.add(cachedEpoch) 197 lookup.mu.Unlock() 198 if err != nil { 199 return fmt.Errorf("could not add epoch %d: %w", counter, err) 200 } 201 return nil 202 } 203 204 // EpochForViewWithFallback returns the counter of the epoch that the input view belongs to. 205 // If epoch fallback has been triggered, returns the last committed epoch counter 206 // in perpetuity for any inputs beyond the last committed epoch view range. 207 // For example, if we trigger epoch fallback during epoch 10, and reach the final 208 // view of epoch 10 before epoch 11 has finished being setup, this function will 209 // return 10 even for input views beyond the final view of epoch 10. 210 // 211 // Returns model.ErrViewForUnknownEpoch if the input does not fall within the range of a known epoch. 212 func (lookup *EpochLookup) EpochForViewWithFallback(view uint64) (uint64, error) { 213 lookup.mu.RLock() 214 defer lookup.mu.RUnlock() 215 firstView, finalView := lookup.epochs.combinedRange() 216 217 // LEGEND: 218 // * -> view argument 219 // [----| -> epoch view range 220 221 // view is before any known epochs 222 // ---*---[----|----|----]------- 223 if view < firstView { 224 return 0, model.ErrViewForUnknownEpoch 225 } 226 // view is after any known epochs 227 // -------[----|----|----]---*--- 228 if view > finalView { 229 // if epoch fallback is triggered, we treat this view as part of the last committed epoch 230 if lookup.epochFallbackIsTriggered.Load() { 231 return lookup.epochs.latest().counter, nil 232 } 233 // otherwise, we are waiting for the epoch including this view to be committed 234 return 0, model.ErrViewForUnknownEpoch 235 } 236 237 // view is within a known epoch 238 for _, epoch := range lookup.epochs { 239 if !epoch.exists() { 240 continue 241 } 242 if epoch.firstView <= view && view <= epoch.finalView { 243 return epoch.counter, nil 244 } 245 } 246 247 // reaching this point indicates a corrupted state or internal bug 248 return 0, fmt.Errorf("sanity check failed: cached epochs (%v) does not contain input view %d", lookup.epochs, view) 249 } 250 251 // handleProtocolEvents processes queued Epoch events `EpochCommittedPhaseStarted` 252 // and `EpochEmergencyFallbackTriggered`. This function permanently utilizes a worker 253 // routine until the `Component` terminates. 254 // When we observe a new epoch being committed, we compute 255 // the leader selection and cache static info for the epoch. When we observe 256 // epoch emergency fallback being triggered, we inject a fallback epoch. 257 func (lookup *EpochLookup) handleProtocolEvents(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) { 258 ready() 259 260 for { 261 select { 262 case <-ctx.Done(): 263 return 264 case block := <-lookup.committedEpochsCh: 265 epoch := lookup.state.AtBlockID(block.ID()).Epochs().Next() 266 err := lookup.cacheEpoch(epoch) 267 if err != nil { 268 ctx.Throw(err) 269 } 270 } 271 } 272 } 273 274 // EpochCommittedPhaseStarted informs the `committee.Consensus` that the block starting the Epoch Committed Phase has been finalized. 275 func (lookup *EpochLookup) EpochCommittedPhaseStarted(_ uint64, first *flow.Header) { 276 lookup.committedEpochsCh <- first 277 } 278 279 // EpochEmergencyFallbackTriggered passes the protocol event to the worker thread. 280 func (lookup *EpochLookup) EpochEmergencyFallbackTriggered() { 281 lookup.epochFallbackIsTriggered.Store(true) 282 }