github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/state/protocol/badger/snapshot.go (about)

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