github.com/onflow/flow-go@v0.33.17/state/protocol/badger/snapshot.go (about)

     1  // (c) 2019 Dapper Labs - ALL RIGHTS RESERVED
     2  
     3  package badger
     4  
     5  import (
     6  	"errors"
     7  	"fmt"
     8  
     9  	"github.com/dgraph-io/badger/v2"
    10  
    11  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    12  	"github.com/onflow/flow-go/model/flow"
    13  	"github.com/onflow/flow-go/model/flow/filter"
    14  	"github.com/onflow/flow-go/model/flow/mapfunc"
    15  	"github.com/onflow/flow-go/state/fork"
    16  	"github.com/onflow/flow-go/state/protocol"
    17  	"github.com/onflow/flow-go/state/protocol/inmem"
    18  	"github.com/onflow/flow-go/state/protocol/invalid"
    19  	"github.com/onflow/flow-go/storage"
    20  	"github.com/onflow/flow-go/storage/badger/operation"
    21  	"github.com/onflow/flow-go/storage/badger/procedure"
    22  )
    23  
    24  // Snapshot implements the protocol.Snapshot interface.
    25  // It represents a read-only immutable snapshot of the protocol state at the
    26  // block it is constructed with. It allows efficient access to data associated directly
    27  // with blocks at a given state (finalized, sealed), such as the related header, commit,
    28  // seed or descending blocks. A block snapshot can lazily convert to an epoch snapshot in
    29  // order to make data associated directly with epochs accessible through its API.
    30  type Snapshot struct {
    31  	state   *State
    32  	blockID flow.Identifier // reference block for this snapshot
    33  }
    34  
    35  // FinalizedSnapshot represents a read-only immutable snapshot of the protocol state
    36  // at a finalized block. It is guaranteed to have a header available.
    37  type FinalizedSnapshot struct {
    38  	Snapshot
    39  	header *flow.Header
    40  }
    41  
    42  var _ protocol.Snapshot = (*Snapshot)(nil)
    43  var _ protocol.Snapshot = (*FinalizedSnapshot)(nil)
    44  
    45  // newSnapshotWithIncorporatedReferenceBlock creates a new state snapshot with the given reference block.
    46  // CAUTION: The caller is responsible for ensuring that the reference block has been incorporated.
    47  func newSnapshotWithIncorporatedReferenceBlock(state *State, blockID flow.Identifier) *Snapshot {
    48  	return &Snapshot{
    49  		state:   state,
    50  		blockID: blockID,
    51  	}
    52  }
    53  
    54  // NewFinalizedSnapshot instantiates a `FinalizedSnapshot`.
    55  // CAUTION: the header's ID _must_ match `blockID` (not checked)
    56  func NewFinalizedSnapshot(state *State, blockID flow.Identifier, header *flow.Header) *FinalizedSnapshot {
    57  	return &FinalizedSnapshot{
    58  		Snapshot: Snapshot{
    59  			state:   state,
    60  			blockID: blockID,
    61  		},
    62  		header: header,
    63  	}
    64  }
    65  
    66  func (s *FinalizedSnapshot) Head() (*flow.Header, error) {
    67  	return s.header, nil
    68  }
    69  
    70  func (s *Snapshot) Head() (*flow.Header, error) {
    71  	head, err := s.state.headers.ByBlockID(s.blockID)
    72  	return head, err
    73  }
    74  
    75  // QuorumCertificate (QC) returns a valid quorum certificate pointing to the
    76  // header at this snapshot.
    77  // The sentinel error storage.ErrNotFound is returned if the QC is unknown.
    78  func (s *Snapshot) QuorumCertificate() (*flow.QuorumCertificate, error) {
    79  	qc, err := s.state.qcs.ByBlockID(s.blockID)
    80  	if err != nil {
    81  		return nil, fmt.Errorf("could not retrieve quorum certificate for (%x): %w", s.blockID, err)
    82  	}
    83  	return qc, nil
    84  }
    85  
    86  func (s *Snapshot) Phase() (flow.EpochPhase, error) {
    87  	status, err := s.state.epoch.statuses.ByBlockID(s.blockID)
    88  	if err != nil {
    89  		return flow.EpochPhaseUndefined, fmt.Errorf("could not retrieve epoch status: %w", err)
    90  	}
    91  	phase, err := status.Phase()
    92  	return phase, err
    93  }
    94  
    95  func (s *Snapshot) Identities(selector flow.IdentityFilter) (flow.IdentityList, error) {
    96  
    97  	// TODO: CAUTION SHORTCUT
    98  	// we retrieve identities based on the initial identity table from the EpochSetup
    99  	// event here -- this will need revision to support mid-epoch identity changes
   100  	// once slashing is implemented
   101  
   102  	status, err := s.state.epoch.statuses.ByBlockID(s.blockID)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	setup, err := s.state.epoch.setups.ByID(status.CurrentEpoch.SetupID)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	// sort the identities so the 'IsCached' binary search works
   113  	identities := setup.Participants.Sort(flow.Canonical)
   114  
   115  	// get identities that are in either last/next epoch but NOT in the current epoch
   116  	var otherEpochIdentities flow.IdentityList
   117  	phase, err := status.Phase()
   118  	if err != nil {
   119  		return nil, fmt.Errorf("could not get phase: %w", err)
   120  	}
   121  	switch phase {
   122  	// during staking phase (the beginning of the epoch) we include identities
   123  	// from the previous epoch that are now un-staking
   124  	case flow.EpochPhaseStaking:
   125  
   126  		if !status.HasPrevious() {
   127  			break
   128  		}
   129  
   130  		previousSetup, err := s.state.epoch.setups.ByID(status.PreviousEpoch.SetupID)
   131  		if err != nil {
   132  			return nil, fmt.Errorf("could not get previous epoch setup event: %w", err)
   133  		}
   134  
   135  		for _, identity := range previousSetup.Participants {
   136  			exists := identities.Exists(identity)
   137  			// add identity from previous epoch that is not in current epoch
   138  			if !exists {
   139  				otherEpochIdentities = append(otherEpochIdentities, identity)
   140  			}
   141  		}
   142  
   143  	// during setup and committed phases (the end of the epoch) we include
   144  	// identities that will join in the next epoch
   145  	case flow.EpochPhaseSetup, flow.EpochPhaseCommitted:
   146  
   147  		nextSetup, err := s.state.epoch.setups.ByID(status.NextEpoch.SetupID)
   148  		if err != nil {
   149  			return nil, fmt.Errorf("could not get next epoch setup: %w", err)
   150  		}
   151  
   152  		for _, identity := range nextSetup.Participants {
   153  			exists := identities.Exists(identity)
   154  
   155  			// add identity from next epoch that is not in current epoch
   156  			if !exists {
   157  				otherEpochIdentities = append(otherEpochIdentities, identity)
   158  			}
   159  		}
   160  
   161  	default:
   162  		return nil, fmt.Errorf("invalid epoch phase: %s", phase)
   163  	}
   164  
   165  	// add the identities from next/last epoch, with weight set to 0
   166  	identities = append(
   167  		identities,
   168  		otherEpochIdentities.Map(mapfunc.WithWeight(0))...,
   169  	)
   170  
   171  	// apply the filter to the participants
   172  	identities = identities.Filter(selector)
   173  
   174  	// apply a deterministic sort to the participants
   175  	identities = identities.Sort(flow.Canonical)
   176  
   177  	return identities, nil
   178  }
   179  
   180  func (s *Snapshot) Identity(nodeID flow.Identifier) (*flow.Identity, error) {
   181  	// filter identities at snapshot for node ID
   182  	identities, err := s.Identities(filter.HasNodeID(nodeID))
   183  	if err != nil {
   184  		return nil, fmt.Errorf("could not get identities: %w", err)
   185  	}
   186  
   187  	// check if node ID is part of identities
   188  	if len(identities) == 0 {
   189  		return nil, protocol.IdentityNotFoundError{NodeID: nodeID}
   190  	}
   191  	return identities[0], nil
   192  }
   193  
   194  // Commit retrieves the latest execution state commitment at the current block snapshot. This
   195  // commitment represents the execution state as currently finalized.
   196  func (s *Snapshot) Commit() (flow.StateCommitment, error) {
   197  	// get the ID of the sealed block
   198  	seal, err := s.state.seals.HighestInFork(s.blockID)
   199  	if err != nil {
   200  		return flow.DummyStateCommitment, fmt.Errorf("could not retrieve sealed state commit: %w", err)
   201  	}
   202  	return seal.FinalState, nil
   203  }
   204  
   205  func (s *Snapshot) SealedResult() (*flow.ExecutionResult, *flow.Seal, error) {
   206  	seal, err := s.state.seals.HighestInFork(s.blockID)
   207  	if err != nil {
   208  		return nil, nil, fmt.Errorf("could not look up latest seal: %w", err)
   209  	}
   210  	result, err := s.state.results.ByID(seal.ResultID)
   211  	if err != nil {
   212  		return nil, nil, fmt.Errorf("could not get latest result: %w", err)
   213  	}
   214  	return result, seal, nil
   215  }
   216  
   217  // SealingSegment will walk through the chain backward until we reach the block referenced
   218  // by the latest seal and build a SealingSegment. As we visit each block we check each execution
   219  // receipt in the block's payload to make sure we have a corresponding execution result, any
   220  // execution results missing from blocks are stored in the `SealingSegment.ExecutionResults` field.
   221  // See `model/flow/sealing_segment.md` for detailed technical specification of the Sealing Segment
   222  //
   223  // Expected errors during normal operations:
   224  //   - protocol.ErrSealingSegmentBelowRootBlock if sealing segment would stretch beyond the node's local history cut-off
   225  //   - protocol.UnfinalizedSealingSegmentError if sealing segment would contain unfinalized blocks (including orphaned blocks)
   226  func (s *Snapshot) SealingSegment() (*flow.SealingSegment, error) {
   227  	// Lets denote the highest block in the sealing segment `head` (initialized below).
   228  	// Based on the tech spec `flow/sealing_segment.md`, the Sealing Segment must contain contain
   229  	//  enough history to satisfy _all_ of the following conditions:
   230  	//   (i) The highest sealed block as of `head` needs to be included in the sealing segment.
   231  	//       This is relevant if `head` does not contain any seals.
   232  	//  (ii) All blocks that are sealed by `head`. This is relevant if head` contains _multiple_ seals.
   233  	// (iii) The sealing segment should contain the history back to (including):
   234  	//       limitHeight := max(blockSealedAtHead.Height - flow.ExtraBlocksInRootSealingSegment, SporkRootBlockHeight)
   235  	// Per convention, we include the blocks for (i) in the `SealingSegment.Blocks`, while the
   236  	// additional blocks for (ii) and optionally (iii) are contained in as `SealingSegment.ExtraBlocks`.
   237  	head, err := s.state.blocks.ByID(s.blockID)
   238  	if err != nil {
   239  		return nil, fmt.Errorf("could not get snapshot's reference block: %w", err)
   240  	}
   241  	if head.Header.Height < s.state.finalizedRootHeight {
   242  		return nil, protocol.ErrSealingSegmentBelowRootBlock
   243  	}
   244  
   245  	// Verify that head of sealing segment is finalized.
   246  	finalizedBlockAtHeight, err := s.state.headers.BlockIDByHeight(head.Header.Height)
   247  	if err != nil {
   248  		if errors.Is(err, storage.ErrNotFound) {
   249  			return nil, protocol.NewUnfinalizedSealingSegmentErrorf("head of sealing segment at height %d is not finalized: %w", head.Header.Height, err)
   250  		}
   251  		return nil, fmt.Errorf("exception while retrieving finzalized bloc, by height: %w", err)
   252  	}
   253  	if finalizedBlockAtHeight != s.blockID { // comparison of fixed-length arrays
   254  		return nil, protocol.NewUnfinalizedSealingSegmentErrorf("head of sealing segment is orphaned, finalized block at height %d is %x", head.Header.Height, finalizedBlockAtHeight)
   255  	}
   256  
   257  	// STEP (i): highest sealed block as of `head` must be included.
   258  	seal, err := s.state.seals.HighestInFork(s.blockID)
   259  	if err != nil {
   260  		return nil, fmt.Errorf("could not get seal for sealing segment: %w", err)
   261  	}
   262  	blockSealedAtHead, err := s.state.headers.ByBlockID(seal.BlockID)
   263  	if err != nil {
   264  		return nil, fmt.Errorf("could not get block: %w", err)
   265  	}
   266  
   267  	// walk through the chain backward until we reach the block referenced by
   268  	// the latest seal - the returned segment includes this block
   269  	builder := flow.NewSealingSegmentBuilder(s.state.results.ByID, s.state.seals.HighestInFork)
   270  	scraper := func(header *flow.Header) error {
   271  		blockID := header.ID()
   272  		block, err := s.state.blocks.ByID(blockID)
   273  		if err != nil {
   274  			return fmt.Errorf("could not get block: %w", err)
   275  		}
   276  
   277  		err = builder.AddBlock(block)
   278  		if err != nil {
   279  			return fmt.Errorf("could not add block to sealing segment: %w", err)
   280  		}
   281  
   282  		return nil
   283  	}
   284  	err = fork.TraverseForward(s.state.headers, s.blockID, scraper, fork.IncludingBlock(seal.BlockID))
   285  	if err != nil {
   286  		return nil, fmt.Errorf("could not traverse sealing segment: %w", err)
   287  	}
   288  
   289  	// STEP (ii): extend history down to the lowest block, whose seal is included in `head`
   290  	lowestSealedByHead := blockSealedAtHead
   291  	for _, sealInHead := range head.Payload.Seals {
   292  		h, e := s.state.headers.ByBlockID(sealInHead.BlockID)
   293  		if e != nil {
   294  			return nil, fmt.Errorf("could not get block (id=%x) for seal: %w", seal.BlockID, e) // storage.ErrNotFound or exception
   295  		}
   296  		if h.Height < lowestSealedByHead.Height {
   297  			lowestSealedByHead = h
   298  		}
   299  	}
   300  
   301  	// STEP (iii): extended history to allow checking for duplicated collections, i.e.
   302  	// limitHeight = max(blockSealedAtHead.Height - flow.ExtraBlocksInRootSealingSegment, SporkRootBlockHeight)
   303  	limitHeight := s.state.sporkRootBlockHeight
   304  	if blockSealedAtHead.Height > s.state.sporkRootBlockHeight+flow.ExtraBlocksInRootSealingSegment {
   305  		limitHeight = blockSealedAtHead.Height - flow.ExtraBlocksInRootSealingSegment
   306  	}
   307  
   308  	// As we have to satisfy (ii) _and_ (iii), we have to take the longest history, i.e. the lowest height.
   309  	if lowestSealedByHead.Height < limitHeight {
   310  		limitHeight = lowestSealedByHead.Height
   311  		if limitHeight < s.state.sporkRootBlockHeight { // sanity check; should never happen
   312  			return nil, fmt.Errorf("unexpected internal error: calculated history-cutoff at height %d, which is lower than the spork's root height %d", limitHeight, s.state.sporkRootBlockHeight)
   313  		}
   314  	}
   315  	if limitHeight < blockSealedAtHead.Height {
   316  		// we need to include extra blocks in sealing segment
   317  		extraBlocksScraper := func(header *flow.Header) error {
   318  			blockID := header.ID()
   319  			block, err := s.state.blocks.ByID(blockID)
   320  			if err != nil {
   321  				return fmt.Errorf("could not get block: %w", err)
   322  			}
   323  
   324  			err = builder.AddExtraBlock(block)
   325  			if err != nil {
   326  				return fmt.Errorf("could not add block to sealing segment: %w", err)
   327  			}
   328  
   329  			return nil
   330  		}
   331  
   332  		err = fork.TraverseBackward(s.state.headers, blockSealedAtHead.ParentID, extraBlocksScraper, fork.IncludingHeight(limitHeight))
   333  		if err != nil {
   334  			return nil, fmt.Errorf("could not traverse extra blocks for sealing segment: %w", err)
   335  		}
   336  	}
   337  
   338  	segment, err := builder.SealingSegment()
   339  	if err != nil {
   340  		return nil, fmt.Errorf("could not build sealing segment: %w", err)
   341  	}
   342  
   343  	return segment, nil
   344  }
   345  
   346  func (s *Snapshot) Descendants() ([]flow.Identifier, error) {
   347  	descendants, err := s.descendants(s.blockID)
   348  	if err != nil {
   349  		return nil, fmt.Errorf("failed to traverse the descendants tree of block %v: %w", s.blockID, err)
   350  	}
   351  	return descendants, nil
   352  }
   353  
   354  func (s *Snapshot) lookupChildren(blockID flow.Identifier) ([]flow.Identifier, error) {
   355  	var children flow.IdentifierList
   356  	err := s.state.db.View(procedure.LookupBlockChildren(blockID, &children))
   357  	if err != nil {
   358  		return nil, fmt.Errorf("could not get children of block %v: %w", blockID, err)
   359  	}
   360  	return children, nil
   361  }
   362  
   363  func (s *Snapshot) descendants(blockID flow.Identifier) ([]flow.Identifier, error) {
   364  	descendantIDs, err := s.lookupChildren(blockID)
   365  	if err != nil {
   366  		return nil, err
   367  	}
   368  
   369  	for _, descendantID := range descendantIDs {
   370  		additionalIDs, err := s.descendants(descendantID)
   371  		if err != nil {
   372  			return nil, err
   373  		}
   374  		descendantIDs = append(descendantIDs, additionalIDs...)
   375  	}
   376  	return descendantIDs, nil
   377  }
   378  
   379  // RandomSource returns the seed for the current block's snapshot.
   380  // Expected error returns:
   381  // * storage.ErrNotFound is returned if the QC is unknown.
   382  func (s *Snapshot) RandomSource() ([]byte, error) {
   383  	qc, err := s.QuorumCertificate()
   384  	if err != nil {
   385  		return nil, err
   386  	}
   387  	randomSource, err := model.BeaconSignature(qc)
   388  	if err != nil {
   389  		return nil, fmt.Errorf("could not create seed from QC's signature: %w", err)
   390  	}
   391  	return randomSource, nil
   392  }
   393  
   394  func (s *Snapshot) Epochs() protocol.EpochQuery {
   395  	return &EpochQuery{
   396  		snap: s,
   397  	}
   398  }
   399  
   400  func (s *Snapshot) Params() protocol.GlobalParams {
   401  	return s.state.Params()
   402  }
   403  
   404  func (s *Snapshot) VersionBeacon() (*flow.SealedVersionBeacon, error) {
   405  	head, err := s.state.headers.ByBlockID(s.blockID)
   406  	if err != nil {
   407  		return nil, err
   408  	}
   409  
   410  	return s.state.versionBeacons.Highest(head.Height)
   411  }
   412  
   413  // EpochQuery encapsulates querying epochs w.r.t. a snapshot.
   414  type EpochQuery struct {
   415  	snap *Snapshot
   416  }
   417  
   418  // Current returns the current epoch.
   419  func (q *EpochQuery) Current() protocol.Epoch {
   420  	// all errors returned from storage reads here are unexpected, because all
   421  	// snapshots reside within a current epoch, which must be queryable
   422  	status, err := q.snap.state.epoch.statuses.ByBlockID(q.snap.blockID)
   423  	if err != nil {
   424  		return invalid.NewEpochf("could not get epoch status for block %x: %w", q.snap.blockID, err)
   425  	}
   426  	setup, err := q.snap.state.epoch.setups.ByID(status.CurrentEpoch.SetupID)
   427  	if err != nil {
   428  		return invalid.NewEpochf("could not get current EpochSetup (id=%x) for block %x: %w", status.CurrentEpoch.SetupID, q.snap.blockID, err)
   429  	}
   430  	commit, err := q.snap.state.epoch.commits.ByID(status.CurrentEpoch.CommitID)
   431  	if err != nil {
   432  		return invalid.NewEpochf("could not get current EpochCommit (id=%x) for block %x: %w", status.CurrentEpoch.CommitID, q.snap.blockID, err)
   433  	}
   434  
   435  	firstHeight, _, epochStarted, _, err := q.retrieveEpochHeightBounds(setup.Counter)
   436  	if err != nil {
   437  		return invalid.NewEpochf("could not get current epoch height bounds: %s", err.Error())
   438  	}
   439  	if epochStarted {
   440  		return inmem.NewStartedEpoch(setup, commit, firstHeight)
   441  	}
   442  	return inmem.NewCommittedEpoch(setup, commit)
   443  }
   444  
   445  // Next returns the next epoch, if it is available.
   446  func (q *EpochQuery) Next() protocol.Epoch {
   447  
   448  	status, err := q.snap.state.epoch.statuses.ByBlockID(q.snap.blockID)
   449  	if err != nil {
   450  		return invalid.NewEpochf("could not get epoch status for block %x: %w", q.snap.blockID, err)
   451  	}
   452  	phase, err := status.Phase()
   453  	if err != nil {
   454  		// critical error: malformed EpochStatus in storage
   455  		return invalid.NewEpochf("read malformed EpochStatus from storage: %w", err)
   456  	}
   457  	// if we are in the staking phase, the next epoch is not setup yet
   458  	if phase == flow.EpochPhaseStaking {
   459  		return invalid.NewEpoch(protocol.ErrNextEpochNotSetup)
   460  	}
   461  
   462  	// if we are in setup phase, return a SetupEpoch
   463  	nextSetup, err := q.snap.state.epoch.setups.ByID(status.NextEpoch.SetupID)
   464  	if err != nil {
   465  		// all errors are critical, because we must be able to retrieve EpochSetup when in setup phase
   466  		return invalid.NewEpochf("could not get next EpochSetup (id=%x) for block %x: %w", status.NextEpoch.SetupID, q.snap.blockID, err)
   467  	}
   468  	if phase == flow.EpochPhaseSetup {
   469  		return inmem.NewSetupEpoch(nextSetup)
   470  	}
   471  
   472  	// if we are in committed phase, return a CommittedEpoch
   473  	nextCommit, err := q.snap.state.epoch.commits.ByID(status.NextEpoch.CommitID)
   474  	if err != nil {
   475  		// all errors are critical, because we must be able to retrieve EpochCommit when in committed phase
   476  		return invalid.NewEpochf("could not get next EpochCommit (id=%x) for block %x: %w", status.NextEpoch.CommitID, q.snap.blockID, err)
   477  	}
   478  	return inmem.NewCommittedEpoch(nextSetup, nextCommit)
   479  }
   480  
   481  // Previous returns the previous epoch. During the first epoch after the root
   482  // block, this returns a sentinel error (since there is no previous epoch).
   483  // For all other epochs, returns the previous epoch.
   484  func (q *EpochQuery) Previous() protocol.Epoch {
   485  
   486  	status, err := q.snap.state.epoch.statuses.ByBlockID(q.snap.blockID)
   487  	if err != nil {
   488  		return invalid.NewEpochf("could not get epoch status for block %x: %w", q.snap.blockID, err)
   489  	}
   490  
   491  	// CASE 1: there is no previous epoch - this indicates we are in the first
   492  	// epoch after a spork root or genesis block
   493  	if !status.HasPrevious() {
   494  		return invalid.NewEpoch(protocol.ErrNoPreviousEpoch)
   495  	}
   496  
   497  	// CASE 2: we are in any other epoch - retrieve the setup and commit events
   498  	// for the previous epoch
   499  	setup, err := q.snap.state.epoch.setups.ByID(status.PreviousEpoch.SetupID)
   500  	if err != nil {
   501  		// all errors are critical, because we must be able to retrieve EpochSetup for previous epoch
   502  		return invalid.NewEpochf("could not get previous EpochSetup (id=%x) for block %x: %w", status.PreviousEpoch.SetupID, q.snap.blockID, err)
   503  	}
   504  	commit, err := q.snap.state.epoch.commits.ByID(status.PreviousEpoch.CommitID)
   505  	if err != nil {
   506  		// all errors are critical, because we must be able to retrieve EpochCommit for previous epoch
   507  		return invalid.NewEpochf("could not get current EpochCommit (id=%x) for block %x: %w", status.PreviousEpoch.CommitID, q.snap.blockID, err)
   508  	}
   509  
   510  	firstHeight, finalHeight, _, epochEnded, err := q.retrieveEpochHeightBounds(setup.Counter)
   511  	if err != nil {
   512  		return invalid.NewEpochf("could not get epoch height bounds: %w", err)
   513  	}
   514  	if epochEnded {
   515  		return inmem.NewEndedEpoch(setup, commit, firstHeight, finalHeight)
   516  	}
   517  	return inmem.NewStartedEpoch(setup, commit, firstHeight)
   518  }
   519  
   520  // retrieveEpochHeightBounds retrieves the height bounds for an epoch.
   521  // Height bounds are NOT fork-aware, and are only determined upon finalization.
   522  //
   523  // Since the protocol state's API is fork-aware, we may be querying an
   524  // un-finalized block, as in the following example:
   525  //
   526  //	Epoch 1    Epoch 2
   527  //	A <- B <-|- C <- D
   528  //
   529  // Suppose block B is the latest finalized block and we have queried block D.
   530  // Then, the transition from epoch 1 to 2 has not been committed, because the first block of epoch 2 has not been finalized.
   531  // In this case, the final block of Epoch 1, from the perspective of block D, is unknown.
   532  // There are edge-case scenarios, where a different fork could exist (as illustrated below)
   533  // that still adds additional blocks to Epoch 1.
   534  //
   535  //	Epoch 1      Epoch 2
   536  //	A <- B <---|-- C <- D
   537  //	     ^
   538  //	     ╰ X <-|- X <- Y <- Z
   539  //
   540  // Returns:
   541  //   - (0, 0, false, false, nil) if epoch is not started
   542  //   - (firstHeight, 0, true, false, nil) if epoch is started but not ended
   543  //   - (firstHeight, finalHeight, true, true, nil) if epoch is ended
   544  //
   545  // No errors are expected during normal operation.
   546  func (q *EpochQuery) retrieveEpochHeightBounds(epoch uint64) (firstHeight, finalHeight uint64, isFirstBlockFinalized, isLastBlockFinalized bool, err error) {
   547  	err = q.snap.state.db.View(func(tx *badger.Txn) error {
   548  		// Retrieve the epoch's first height
   549  		err = operation.RetrieveEpochFirstHeight(epoch, &firstHeight)(tx)
   550  		if err != nil {
   551  			if errors.Is(err, storage.ErrNotFound) {
   552  				isFirstBlockFinalized = false
   553  				isLastBlockFinalized = false
   554  				return nil
   555  			}
   556  			return err // unexpected error
   557  		}
   558  		isFirstBlockFinalized = true
   559  
   560  		var subsequentEpochFirstHeight uint64
   561  		err = operation.RetrieveEpochFirstHeight(epoch+1, &subsequentEpochFirstHeight)(tx)
   562  		if err != nil {
   563  			if errors.Is(err, storage.ErrNotFound) {
   564  				isLastBlockFinalized = false
   565  				return nil
   566  			}
   567  			return err // unexpected error
   568  		}
   569  		finalHeight = subsequentEpochFirstHeight - 1
   570  		isLastBlockFinalized = true
   571  
   572  		return nil
   573  	})
   574  	if err != nil {
   575  		return 0, 0, false, false, err
   576  	}
   577  	return firstHeight, finalHeight, isFirstBlockFinalized, isLastBlockFinalized, nil
   578  }