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 }