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  }