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

     1  package backend
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  
     8  	"google.golang.org/grpc/codes"
     9  	"google.golang.org/grpc/status"
    10  
    11  	"github.com/rs/zerolog"
    12  
    13  	"github.com/onflow/flow-go/access"
    14  	"github.com/onflow/flow-go/engine/access/subscription"
    15  	"github.com/onflow/flow-go/engine/common/rpc"
    16  	"github.com/onflow/flow-go/model/flow"
    17  	"github.com/onflow/flow-go/module/irrecoverable"
    18  	"github.com/onflow/flow-go/state"
    19  	"github.com/onflow/flow-go/storage"
    20  
    21  	"github.com/onflow/flow/protobuf/go/flow/entities"
    22  )
    23  
    24  // backendSubscribeTransactions handles transaction subscriptions.
    25  type backendSubscribeTransactions struct {
    26  	txLocalDataProvider *TransactionsLocalDataProvider
    27  	backendTransactions *backendTransactions
    28  	executionResults    storage.ExecutionResults
    29  	log                 zerolog.Logger
    30  
    31  	subscriptionHandler *subscription.SubscriptionHandler
    32  	blockTracker        subscription.BlockTracker
    33  }
    34  
    35  // TransactionSubscriptionMetadata holds data representing the status state for each transaction subscription.
    36  type TransactionSubscriptionMetadata struct {
    37  	*access.TransactionResult
    38  	txReferenceBlockID   flow.Identifier
    39  	blockWithTx          *flow.Header
    40  	txExecuted           bool
    41  	eventEncodingVersion entities.EventEncodingVersion
    42  }
    43  
    44  // SubscribeTransactionStatuses subscribes to transaction status changes starting from the transaction reference block ID.
    45  // If invalid tx parameters will be supplied SubscribeTransactionStatuses will return a failed subscription.
    46  func (b *backendSubscribeTransactions) SubscribeTransactionStatuses(
    47  	ctx context.Context,
    48  	tx *flow.TransactionBody,
    49  	requiredEventEncodingVersion entities.EventEncodingVersion,
    50  ) subscription.Subscription {
    51  	nextHeight, err := b.blockTracker.GetStartHeightFromBlockID(tx.ReferenceBlockID)
    52  	if err != nil {
    53  		return subscription.NewFailedSubscription(err, "could not get start height")
    54  	}
    55  
    56  	txInfo := TransactionSubscriptionMetadata{
    57  		TransactionResult: &access.TransactionResult{
    58  			TransactionID: tx.ID(),
    59  			BlockID:       flow.ZeroID,
    60  			Status:        flow.TransactionStatusUnknown,
    61  		},
    62  		txReferenceBlockID:   tx.ReferenceBlockID,
    63  		blockWithTx:          nil,
    64  		eventEncodingVersion: requiredEventEncodingVersion,
    65  	}
    66  
    67  	return b.subscriptionHandler.Subscribe(ctx, nextHeight, b.getTransactionStatusResponse(&txInfo))
    68  }
    69  
    70  // getTransactionStatusResponse returns a callback function that produces transaction status
    71  // subscription responses based on new blocks.
    72  func (b *backendSubscribeTransactions) getTransactionStatusResponse(txInfo *TransactionSubscriptionMetadata) func(context.Context, uint64) (interface{}, error) {
    73  	return func(ctx context.Context, height uint64) (interface{}, error) {
    74  		err := b.checkBlockReady(height)
    75  		if err != nil {
    76  			return nil, err
    77  		}
    78  
    79  		// If the transaction status already reported the final status, return with no data available
    80  		if txInfo.Status == flow.TransactionStatusSealed || txInfo.Status == flow.TransactionStatusExpired {
    81  			return nil, fmt.Errorf("transaction final status %s was already reported: %w", txInfo.Status.String(), subscription.ErrEndOfData)
    82  		}
    83  
    84  		// If on this step transaction block not available, search for it.
    85  		if txInfo.blockWithTx == nil {
    86  			// Search for transaction`s block information.
    87  			txInfo.blockWithTx,
    88  				txInfo.BlockID,
    89  				txInfo.BlockHeight,
    90  				txInfo.CollectionID,
    91  				err = b.searchForTransactionBlockInfo(height, txInfo)
    92  
    93  			if err != nil {
    94  				if errors.Is(err, storage.ErrNotFound) {
    95  					return nil, fmt.Errorf("could not find block %d in storage: %w", height, subscription.ErrBlockNotReady)
    96  				}
    97  
    98  				if !errors.Is(err, ErrTransactionNotInBlock) {
    99  					return nil, status.Errorf(codes.Internal, "could not get block %d: %v", height, err)
   100  				}
   101  			}
   102  		}
   103  
   104  		// Get old status here, as it could be replaced by status from founded tx result
   105  		prevTxStatus := txInfo.Status
   106  
   107  		// Check, if transaction executed and transaction result already available
   108  		if txInfo.blockWithTx != nil && !txInfo.txExecuted {
   109  			txResult, err := b.searchForTransactionResult(ctx, txInfo)
   110  			if err != nil {
   111  				return nil, status.Errorf(codes.Internal, "failed to get execution result for block %s: %v", txInfo.BlockID, err)
   112  			}
   113  
   114  			// If transaction result was found, fully replace it in metadata. New transaction status already included in result.
   115  			if txResult != nil {
   116  				txInfo.TransactionResult = txResult
   117  				//Fill in execution status for future usages
   118  				txInfo.txExecuted = true
   119  			}
   120  		}
   121  
   122  		// If block with transaction was not found, get transaction status to check if it different from last status
   123  		if txInfo.blockWithTx == nil {
   124  			txInfo.Status, err = b.txLocalDataProvider.DeriveUnknownTransactionStatus(txInfo.txReferenceBlockID)
   125  		} else if txInfo.Status == prevTxStatus {
   126  			// When a block with the transaction is available, it is possible to receive a new transaction status while
   127  			// searching for the transaction result. Otherwise, it remains unchanged. So, if the old and new transaction
   128  			// statuses are the same, the current transaction status should be retrieved.
   129  			txInfo.Status, err = b.txLocalDataProvider.DeriveTransactionStatus(txInfo.blockWithTx.Height, txInfo.txExecuted)
   130  		}
   131  		if err != nil {
   132  			if !errors.Is(err, state.ErrUnknownSnapshotReference) {
   133  				irrecoverable.Throw(ctx, err)
   134  			}
   135  			return nil, rpc.ConvertStorageError(err)
   136  		}
   137  
   138  		// If the old and new transaction statuses are still the same, the status change should not be reported, so
   139  		// return here with no response.
   140  		if prevTxStatus == txInfo.Status {
   141  			return nil, nil
   142  		}
   143  
   144  		return b.generateResultsWithMissingStatuses(txInfo, prevTxStatus)
   145  	}
   146  }
   147  
   148  // generateResultsWithMissingStatuses checks if the current result differs from the previous result by more than one step.
   149  // If yes, it generates results for the missing transaction statuses. This is done because the subscription should send
   150  // responses for each of the statuses in the transaction lifecycle, and the message should be sent in the order of transaction statuses.
   151  // Possible orders of transaction statuses:
   152  // 1. pending(1) -> finalized(2) -> executed(3) -> sealed(4)
   153  // 2. pending(1) -> expired(5)
   154  // No errors expected during normal operations.
   155  func (b *backendSubscribeTransactions) generateResultsWithMissingStatuses(
   156  	txInfo *TransactionSubscriptionMetadata,
   157  	prevTxStatus flow.TransactionStatus,
   158  ) ([]*access.TransactionResult, error) {
   159  	// If the previous status is pending and the new status is expired, which is the last status, return its result.
   160  	// If the previous status is anything other than pending, return an error, as this transition is unexpected.
   161  	if txInfo.Status == flow.TransactionStatusExpired {
   162  		if prevTxStatus == flow.TransactionStatusPending {
   163  			return []*access.TransactionResult{
   164  				txInfo.TransactionResult,
   165  			}, nil
   166  		} else {
   167  			return nil, fmt.Errorf("unexpected transition from %s to %s transaction status", prevTxStatus.String(), txInfo.Status.String())
   168  		}
   169  	}
   170  
   171  	var results []*access.TransactionResult
   172  
   173  	// If the difference between statuses' values is more than one step, fill in the missing results.
   174  	if (txInfo.Status - prevTxStatus) > 1 {
   175  		for missingStatus := prevTxStatus + 1; missingStatus < txInfo.Status; missingStatus++ {
   176  			switch missingStatus {
   177  			case flow.TransactionStatusPending:
   178  				results = append(results, &access.TransactionResult{
   179  					Status:        missingStatus,
   180  					TransactionID: txInfo.TransactionID,
   181  				})
   182  			case flow.TransactionStatusFinalized:
   183  				results = append(results, &access.TransactionResult{
   184  					Status:        missingStatus,
   185  					TransactionID: txInfo.TransactionID,
   186  					BlockID:       txInfo.BlockID,
   187  					BlockHeight:   txInfo.BlockHeight,
   188  					CollectionID:  txInfo.CollectionID,
   189  				})
   190  			case flow.TransactionStatusExecuted:
   191  				missingTxResult := *txInfo.TransactionResult
   192  				missingTxResult.Status = missingStatus
   193  				results = append(results, &missingTxResult)
   194  			default:
   195  				return nil, fmt.Errorf("unexpected missing transaction status")
   196  			}
   197  		}
   198  	}
   199  
   200  	results = append(results, txInfo.TransactionResult)
   201  	return results, nil
   202  }
   203  
   204  // checkBlockReady checks if the given block height is valid and available based on the expected block status.
   205  // Expected errors during normal operation:
   206  // - subscription.ErrBlockNotReady: block for the given block height is not available.
   207  func (b *backendSubscribeTransactions) checkBlockReady(height uint64) error {
   208  	// Get the highest available finalized block height
   209  	highestHeight, err := b.blockTracker.GetHighestHeight(flow.BlockStatusFinalized)
   210  	if err != nil {
   211  		return fmt.Errorf("could not get highest height for block %d: %w", height, err)
   212  	}
   213  
   214  	// Fail early if no block finalized notification has been received for the given height.
   215  	// Note: It's possible that the block is locally finalized before the notification is
   216  	// received. This ensures a consistent view is available to all streams.
   217  	if height > highestHeight {
   218  		return fmt.Errorf("block %d is not available yet: %w", height, subscription.ErrBlockNotReady)
   219  	}
   220  
   221  	return nil
   222  }
   223  
   224  // searchForTransactionBlockInfo searches for the block containing the specified transaction.
   225  // It retrieves the block at the given height and checks if the transaction is included in that block.
   226  // Expected errors:
   227  // - ErrTransactionNotInBlock when unable to retrieve the collection
   228  // - codes.Internal when other errors occur during block or collection lookup
   229  func (b *backendSubscribeTransactions) searchForTransactionBlockInfo(
   230  	height uint64,
   231  	txInfo *TransactionSubscriptionMetadata,
   232  ) (*flow.Header, flow.Identifier, uint64, flow.Identifier, error) {
   233  	block, err := b.txLocalDataProvider.blocks.ByHeight(height)
   234  	if err != nil {
   235  		return nil, flow.ZeroID, 0, flow.ZeroID, fmt.Errorf("error looking up block: %w", err)
   236  	}
   237  
   238  	collectionID, err := b.txLocalDataProvider.LookupCollectionIDInBlock(block, txInfo.TransactionID)
   239  	if err != nil {
   240  		return nil, flow.ZeroID, 0, flow.ZeroID, fmt.Errorf("error looking up transaction in block: %w", err)
   241  	}
   242  
   243  	if collectionID != flow.ZeroID {
   244  		return block.Header, block.ID(), height, collectionID, nil
   245  	}
   246  
   247  	return nil, flow.ZeroID, 0, flow.ZeroID, nil
   248  }
   249  
   250  // searchForTransactionResult searches for the transaction result of a block. It retrieves the execution result for the specified block ID.
   251  // Expected errors:
   252  // - codes.Internal if an internal error occurs while retrieving execution result.
   253  func (b *backendSubscribeTransactions) searchForTransactionResult(
   254  	ctx context.Context,
   255  	txInfo *TransactionSubscriptionMetadata,
   256  ) (*access.TransactionResult, error) {
   257  	_, err := b.executionResults.ByBlockID(txInfo.BlockID)
   258  	if err != nil {
   259  		if errors.Is(err, storage.ErrNotFound) {
   260  			return nil, nil
   261  		}
   262  		return nil, fmt.Errorf("failed to get execution result for block %s: %w", txInfo.BlockID, err)
   263  	}
   264  
   265  	txResult, err := b.backendTransactions.GetTransactionResult(
   266  		ctx,
   267  		txInfo.TransactionID,
   268  		txInfo.BlockID,
   269  		txInfo.CollectionID,
   270  		txInfo.eventEncodingVersion,
   271  	)
   272  
   273  	if err != nil {
   274  		// if either the storage or execution node reported no results or there were not enough execution results
   275  		if status.Code(err) == codes.NotFound {
   276  			// No result yet, indicate that it has not been executed
   277  			return nil, nil
   278  		}
   279  		// Other Error trying to retrieve the result, return with err
   280  		return nil, err
   281  	}
   282  
   283  	return txResult, nil
   284  }