github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/rpc/backend/backend_stream_blocks.go (about)

     1  package backend
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  
     8  	"github.com/rs/zerolog"
     9  
    10  	"github.com/onflow/flow-go/engine/access/subscription"
    11  	"github.com/onflow/flow-go/model/flow"
    12  	"github.com/onflow/flow-go/state/protocol"
    13  	"github.com/onflow/flow-go/storage"
    14  	"github.com/onflow/flow-go/utils/logging"
    15  )
    16  
    17  // backendSubscribeBlocks is a struct representing a backend implementation for subscribing to blocks.
    18  type backendSubscribeBlocks struct {
    19  	log     zerolog.Logger
    20  	state   protocol.State
    21  	blocks  storage.Blocks
    22  	headers storage.Headers
    23  
    24  	subscriptionHandler *subscription.SubscriptionHandler
    25  	blockTracker        subscription.BlockTracker
    26  }
    27  
    28  // SubscribeBlocksFromStartBlockID subscribes to the finalized or sealed blocks starting at the requested
    29  // start block id, up until the latest available block. Once the latest is
    30  // reached, the stream will remain open and responses are sent for each new
    31  // block as it becomes available.
    32  //
    33  // Each block is filtered by the provided block status, and only
    34  // those blocks that match the status are returned.
    35  //
    36  // Parameters:
    37  // - ctx: Context for the operation.
    38  // - startBlockID: The identifier of the starting block.
    39  // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized.
    40  //
    41  // If invalid parameters will be supplied SubscribeBlocksFromStartBlockID will return a failed subscription.
    42  func (b *backendSubscribeBlocks) SubscribeBlocksFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription {
    43  	return b.subscribeFromStartBlockID(ctx, startBlockID, b.getBlockResponse(blockStatus))
    44  }
    45  
    46  // SubscribeBlocksFromStartHeight subscribes to the finalized or sealed blocks starting at the requested
    47  // start block height, up until the latest available block. Once the latest is
    48  // reached, the stream will remain open and responses are sent for each new
    49  // block as it becomes available.
    50  //
    51  // Each block is filtered by the provided block status, and only
    52  // those blocks that match the status are returned.
    53  //
    54  // Parameters:
    55  // - ctx: Context for the operation.
    56  // - startHeight: The height of the starting block.
    57  // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized.
    58  //
    59  // If invalid parameters will be supplied SubscribeBlocksFromStartHeight will return a failed subscription.
    60  func (b *backendSubscribeBlocks) SubscribeBlocksFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription {
    61  	return b.subscribeFromStartHeight(ctx, startHeight, b.getBlockResponse(blockStatus))
    62  }
    63  
    64  // SubscribeBlocksFromLatest subscribes to the finalized or sealed blocks starting at the latest sealed block,
    65  // up until the latest available block. Once the latest is
    66  // reached, the stream will remain open and responses are sent for each new
    67  // block as it becomes available.
    68  //
    69  // Each block is filtered by the provided block status, and only
    70  // those blocks that match the status are returned.
    71  //
    72  // Parameters:
    73  // - ctx: Context for the operation.
    74  // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized.
    75  //
    76  // If invalid parameters will be supplied SubscribeBlocksFromLatest will return a failed subscription.
    77  func (b *backendSubscribeBlocks) SubscribeBlocksFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription {
    78  	return b.subscribeFromLatest(ctx, b.getBlockResponse(blockStatus))
    79  }
    80  
    81  // SubscribeBlockHeadersFromStartBlockID streams finalized or sealed block headers starting at the requested
    82  // start block id, up until the latest available block header. Once the latest is
    83  // reached, the stream will remain open and responses are sent for each new
    84  // block header as it becomes available.
    85  //
    86  // Each block header are filtered by the provided block status, and only
    87  // those block headers that match the status are returned.
    88  //
    89  // Parameters:
    90  // - ctx: Context for the operation.
    91  // - startBlockID: The identifier of the starting block.
    92  // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized.
    93  //
    94  // If invalid parameters will be supplied SubscribeBlockHeadersFromStartBlockID will return a failed subscription.
    95  func (b *backendSubscribeBlocks) SubscribeBlockHeadersFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription {
    96  	return b.subscribeFromStartBlockID(ctx, startBlockID, b.getBlockHeaderResponse(blockStatus))
    97  }
    98  
    99  // SubscribeBlockHeadersFromStartHeight streams finalized or sealed block headers starting at the requested
   100  // start block height, up until the latest available block header. Once the latest is
   101  // reached, the stream will remain open and responses are sent for each new
   102  // block header as it becomes available.
   103  //
   104  // Each block header are filtered by the provided block status, and only
   105  // those block headers that match the status are returned.
   106  //
   107  // Parameters:
   108  // - ctx: Context for the operation.
   109  // - startHeight: The height of the starting block.
   110  // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized.
   111  //
   112  // If invalid parameters will be supplied SubscribeBlockHeadersFromStartHeight will return a failed subscription.
   113  func (b *backendSubscribeBlocks) SubscribeBlockHeadersFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription {
   114  	return b.subscribeFromStartHeight(ctx, startHeight, b.getBlockHeaderResponse(blockStatus))
   115  }
   116  
   117  // SubscribeBlockHeadersFromLatest streams finalized or sealed block headers starting at the latest sealed block,
   118  // up until the latest available block header. Once the latest is
   119  // reached, the stream will remain open and responses are sent for each new
   120  // block header as it becomes available.
   121  //
   122  // Each block header are filtered by the provided block status, and only
   123  // those block headers that match the status are returned.
   124  //
   125  // Parameters:
   126  // - ctx: Context for the operation.
   127  // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized.
   128  //
   129  // If invalid parameters will be supplied SubscribeBlockHeadersFromLatest will return a failed subscription.
   130  func (b *backendSubscribeBlocks) SubscribeBlockHeadersFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription {
   131  	return b.subscribeFromLatest(ctx, b.getBlockHeaderResponse(blockStatus))
   132  }
   133  
   134  // SubscribeBlockDigestsFromStartBlockID streams finalized or sealed lightweight block starting at the requested
   135  // start block id, up until the latest available block. Once the latest is
   136  // reached, the stream will remain open and responses are sent for each new
   137  // block as it becomes available.
   138  //
   139  // Each lightweight block are filtered by the provided block status, and only
   140  // those blocks that match the status are returned.
   141  //
   142  // Parameters:
   143  // - ctx: Context for the operation.
   144  // - startBlockID: The identifier of the starting block.
   145  // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized.
   146  //
   147  // If invalid parameters will be supplied SubscribeBlockDigestsFromStartBlockID will return a failed subscription.
   148  func (b *backendSubscribeBlocks) SubscribeBlockDigestsFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription {
   149  	return b.subscribeFromStartBlockID(ctx, startBlockID, b.getBlockDigestResponse(blockStatus))
   150  }
   151  
   152  // SubscribeBlockDigestsFromStartHeight streams finalized or sealed lightweight block starting at the requested
   153  // start block height, up until the latest available block. Once the latest is
   154  // reached, the stream will remain open and responses are sent for each new
   155  // block as it becomes available.
   156  //
   157  // Each lightweight block are filtered by the provided block status, and only
   158  // those blocks that match the status are returned.
   159  //
   160  // Parameters:
   161  // - ctx: Context for the operation.
   162  // - startHeight: The height of the starting block.
   163  // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized.
   164  //
   165  // If invalid parameters will be supplied SubscribeBlockDigestsFromStartHeight will return a failed subscription.
   166  func (b *backendSubscribeBlocks) SubscribeBlockDigestsFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription {
   167  	return b.subscribeFromStartHeight(ctx, startHeight, b.getBlockDigestResponse(blockStatus))
   168  }
   169  
   170  // SubscribeBlockDigestsFromLatest streams finalized or sealed lightweight block starting at the latest sealed block,
   171  // up until the latest available block. Once the latest is
   172  // reached, the stream will remain open and responses are sent for each new
   173  // block as it becomes available.
   174  //
   175  // Each lightweight block are filtered by the provided block status, and only
   176  // those blocks that match the status are returned.
   177  //
   178  // Parameters:
   179  // - ctx: Context for the operation.
   180  // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized.
   181  //
   182  // If invalid parameters will be supplied SubscribeBlockDigestsFromLatest will return a failed subscription.
   183  func (b *backendSubscribeBlocks) SubscribeBlockDigestsFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription {
   184  	return b.subscribeFromLatest(ctx, b.getBlockDigestResponse(blockStatus))
   185  }
   186  
   187  // subscribeFromStartBlockID is common method that allows clients to subscribe starting at the requested start block id.
   188  //
   189  // Parameters:
   190  // - ctx: Context for the operation.
   191  // - startBlockID: The identifier of the starting block.
   192  // - getData: The callback used by subscriptions to retrieve data information for the specified height and block status.
   193  //
   194  // If invalid parameters are supplied, subscribeFromStartBlockID will return a failed subscription.
   195  func (b *backendSubscribeBlocks) subscribeFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, getData subscription.GetDataByHeightFunc) subscription.Subscription {
   196  	nextHeight, err := b.blockTracker.GetStartHeightFromBlockID(startBlockID)
   197  	if err != nil {
   198  		return subscription.NewFailedSubscription(err, "could not get start height from block id")
   199  	}
   200  	return b.subscriptionHandler.Subscribe(ctx, nextHeight, getData)
   201  }
   202  
   203  // subscribeFromStartHeight is common method that allows clients to subscribe starting at the requested start block height.
   204  //
   205  // Parameters:
   206  // - ctx: Context for the operation.
   207  // - startHeight: The height of the starting block.
   208  // - getData: The callback used by subscriptions to retrieve data information for the specified height and block status.
   209  //
   210  // If invalid parameters are supplied, subscribeFromStartHeight will return a failed subscription.
   211  func (b *backendSubscribeBlocks) subscribeFromStartHeight(ctx context.Context, startHeight uint64, getData subscription.GetDataByHeightFunc) subscription.Subscription {
   212  	nextHeight, err := b.blockTracker.GetStartHeightFromHeight(startHeight)
   213  	if err != nil {
   214  		return subscription.NewFailedSubscription(err, "could not get start height from block height")
   215  	}
   216  	return b.subscriptionHandler.Subscribe(ctx, nextHeight, getData)
   217  }
   218  
   219  // subscribeFromLatest is common method that allows clients to subscribe starting at the latest sealed block.
   220  //
   221  // Parameters:
   222  // - ctx: Context for the operation.
   223  // - getData: The callback used by subscriptions to retrieve data information for the specified height and block status.
   224  //
   225  // No errors are expected during normal operation.
   226  func (b *backendSubscribeBlocks) subscribeFromLatest(ctx context.Context, getData subscription.GetDataByHeightFunc) subscription.Subscription {
   227  	nextHeight, err := b.blockTracker.GetStartHeightFromLatest(ctx)
   228  	if err != nil {
   229  		return subscription.NewFailedSubscription(err, "could not get start height from latest")
   230  	}
   231  	return b.subscriptionHandler.Subscribe(ctx, nextHeight, getData)
   232  }
   233  
   234  // getBlockResponse returns a GetDataByHeightFunc that retrieves block information for the specified height.
   235  func (b *backendSubscribeBlocks) getBlockResponse(blockStatus flow.BlockStatus) subscription.GetDataByHeightFunc {
   236  	return func(_ context.Context, height uint64) (interface{}, error) {
   237  		block, err := b.getBlock(height, blockStatus)
   238  		if err != nil {
   239  			return nil, err
   240  		}
   241  
   242  		b.log.Trace().
   243  			Hex("block_id", logging.ID(block.ID())).
   244  			Uint64("height", height).
   245  			Msgf("sending block info")
   246  
   247  		return block, nil
   248  	}
   249  }
   250  
   251  // getBlockHeaderResponse returns a GetDataByHeightFunc that retrieves block header information for the specified height.
   252  func (b *backendSubscribeBlocks) getBlockHeaderResponse(blockStatus flow.BlockStatus) subscription.GetDataByHeightFunc {
   253  	return func(_ context.Context, height uint64) (interface{}, error) {
   254  		header, err := b.getBlockHeader(height, blockStatus)
   255  		if err != nil {
   256  			return nil, err
   257  		}
   258  
   259  		b.log.Trace().
   260  			Hex("block_id", logging.ID(header.ID())).
   261  			Uint64("height", height).
   262  			Msgf("sending block header info")
   263  
   264  		return header, nil
   265  	}
   266  }
   267  
   268  // getBlockDigestResponse returns a GetDataByHeightFunc that retrieves lightweight block information for the specified height.
   269  func (b *backendSubscribeBlocks) getBlockDigestResponse(blockStatus flow.BlockStatus) subscription.GetDataByHeightFunc {
   270  	return func(_ context.Context, height uint64) (interface{}, error) {
   271  		header, err := b.getBlockHeader(height, blockStatus)
   272  		if err != nil {
   273  			return nil, err
   274  		}
   275  
   276  		b.log.Trace().
   277  			Hex("block_id", logging.ID(header.ID())).
   278  			Uint64("height", height).
   279  			Msgf("sending lightweight block info")
   280  
   281  		return flow.NewBlockDigest(header.ID(), header.Height, header.Timestamp), nil
   282  	}
   283  }
   284  
   285  // getBlockHeader returns the block header for the given block height.
   286  // Expected errors during normal operation:
   287  // - subscription.ErrBlockNotReady: block for the given block height is not available.
   288  func (b *backendSubscribeBlocks) getBlockHeader(height uint64, expectedBlockStatus flow.BlockStatus) (*flow.Header, error) {
   289  	err := b.validateHeight(height, expectedBlockStatus)
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  
   294  	// since we are querying a finalized or sealed block header, we can use the height index and save an ID computation
   295  	header, err := b.headers.ByHeight(height)
   296  	if err != nil {
   297  		if errors.Is(err, storage.ErrNotFound) {
   298  			return nil, fmt.Errorf("failed to retrieve block header for height %d: %w", height, subscription.ErrBlockNotReady)
   299  		}
   300  		return nil, err
   301  	}
   302  
   303  	return header, nil
   304  }
   305  
   306  // getBlock returns the block for the given block height.
   307  // Expected errors during normal operation:
   308  // - subscription.ErrBlockNotReady: block for the given block height is not available.
   309  func (b *backendSubscribeBlocks) getBlock(height uint64, expectedBlockStatus flow.BlockStatus) (*flow.Block, error) {
   310  	err := b.validateHeight(height, expectedBlockStatus)
   311  	if err != nil {
   312  		return nil, err
   313  	}
   314  
   315  	// since we are querying a finalized or sealed block, we can use the height index and save an ID computation
   316  	block, err := b.blocks.ByHeight(height)
   317  	if err != nil {
   318  		if errors.Is(err, storage.ErrNotFound) {
   319  			return nil, fmt.Errorf("failed to retrieve block for height %d: %w", height, subscription.ErrBlockNotReady)
   320  		}
   321  		return nil, err
   322  	}
   323  
   324  	return block, nil
   325  }
   326  
   327  // validateHeight checks if the given block height is valid and available based on the expected block status.
   328  // Expected errors during normal operation:
   329  // - subscription.ErrBlockNotReady when unable to retrieve the block by height.
   330  func (b *backendSubscribeBlocks) validateHeight(height uint64, expectedBlockStatus flow.BlockStatus) error {
   331  	highestHeight, err := b.blockTracker.GetHighestHeight(expectedBlockStatus)
   332  	if err != nil {
   333  		return fmt.Errorf("could not get highest available height: %w", err)
   334  	}
   335  
   336  	// fail early if no notification has been received for the given block height.
   337  	// note: it's possible for the data to exist in the data store before the notification is
   338  	// received. this ensures a consistent view is available to all streams.
   339  	if height > highestHeight {
   340  		return fmt.Errorf("block %d is not available yet: %w", height, subscription.ErrBlockNotReady)
   341  	}
   342  
   343  	return nil
   344  }