github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/rpc/backend/backend.go (about) 1 package backend 2 3 import ( 4 "context" 5 "crypto/md5" //nolint:gosec 6 "fmt" 7 "time" 8 9 lru "github.com/hashicorp/golang-lru/v2" 10 accessproto "github.com/onflow/flow/protobuf/go/flow/access" 11 "github.com/rs/zerolog" 12 13 "github.com/onflow/flow-go/access" 14 "github.com/onflow/flow-go/cmd/build" 15 "github.com/onflow/flow-go/engine/access/index" 16 "github.com/onflow/flow-go/engine/access/rpc/connection" 17 "github.com/onflow/flow-go/engine/access/subscription" 18 "github.com/onflow/flow-go/engine/common/rpc" 19 "github.com/onflow/flow-go/fvm/blueprints" 20 "github.com/onflow/flow-go/model/flow" 21 "github.com/onflow/flow-go/model/flow/filter" 22 "github.com/onflow/flow-go/module" 23 "github.com/onflow/flow-go/module/counters" 24 "github.com/onflow/flow-go/module/execution" 25 "github.com/onflow/flow-go/state/protocol" 26 "github.com/onflow/flow-go/storage" 27 ) 28 29 // minExecutionNodesCnt is the minimum number of execution nodes expected to have sent the execution receipt for a block 30 const minExecutionNodesCnt = 2 31 32 // maxAttemptsForExecutionReceipt is the maximum number of attempts to find execution receipts for a given block ID 33 const maxAttemptsForExecutionReceipt = 3 34 35 // DefaultMaxHeightRange is the default maximum size of range requests. 36 const DefaultMaxHeightRange = 250 37 38 // DefaultSnapshotHistoryLimit the amount of blocks to look back in state 39 // when recursively searching for a valid snapshot 40 const DefaultSnapshotHistoryLimit = 500 41 42 // DefaultLoggedScriptsCacheSize is the default size of the lookup cache used to dedupe logs of scripts sent to ENs 43 // limiting cache size to 16MB and does not affect script execution, only for keeping logs tidy 44 const DefaultLoggedScriptsCacheSize = 1_000_000 45 46 // DefaultConnectionPoolSize is the default size for the connection pool to collection and execution nodes 47 const DefaultConnectionPoolSize = 250 48 49 var ( 50 preferredENIdentifiers flow.IdentifierList 51 fixedENIdentifiers flow.IdentifierList 52 ) 53 54 // Backend implements the Access API. 55 // 56 // It is composed of several sub-backends that implement part of the Access API. 57 // 58 // Script related calls are handled by backendScripts. 59 // Transaction related calls are handled by backendTransactions. 60 // Block Header related calls are handled by backendBlockHeaders. 61 // Block details related calls are handled by backendBlockDetails. 62 // Event related calls are handled by backendEvents. 63 // Account related calls are handled by backendAccounts. 64 // 65 // All remaining calls are handled by the base Backend in this file. 66 type Backend struct { 67 backendScripts 68 backendTransactions 69 backendEvents 70 backendBlockHeaders 71 backendBlockDetails 72 backendAccounts 73 backendExecutionResults 74 backendNetwork 75 backendSubscribeBlocks 76 backendSubscribeTransactions 77 78 state protocol.State 79 chainID flow.ChainID 80 collections storage.Collections 81 executionReceipts storage.ExecutionReceipts 82 connFactory connection.ConnectionFactory 83 84 // cache the response to GetNodeVersionInfo since it doesn't change 85 nodeInfo *access.NodeVersionInfo 86 BlockTracker subscription.BlockTracker 87 } 88 89 type Params struct { 90 State protocol.State 91 CollectionRPC accessproto.AccessAPIClient 92 HistoricalAccessNodes []accessproto.AccessAPIClient 93 Blocks storage.Blocks 94 Headers storage.Headers 95 Collections storage.Collections 96 Transactions storage.Transactions 97 ExecutionReceipts storage.ExecutionReceipts 98 ExecutionResults storage.ExecutionResults 99 ChainID flow.ChainID 100 AccessMetrics module.AccessMetrics 101 ConnFactory connection.ConnectionFactory 102 RetryEnabled bool 103 MaxHeightRange uint 104 PreferredExecutionNodeIDs []string 105 FixedExecutionNodeIDs []string 106 Log zerolog.Logger 107 SnapshotHistoryLimit int 108 Communicator Communicator 109 TxResultCacheSize uint 110 TxErrorMessagesCacheSize uint 111 ScriptExecutor execution.ScriptExecutor 112 ScriptExecutionMode IndexQueryMode 113 EventQueryMode IndexQueryMode 114 BlockTracker subscription.BlockTracker 115 SubscriptionHandler *subscription.SubscriptionHandler 116 117 EventsIndex *index.EventsIndex 118 TxResultQueryMode IndexQueryMode 119 TxResultsIndex *index.TransactionResultsIndex 120 LastFullBlockHeight *counters.PersistentStrictMonotonicCounter 121 } 122 123 var _ TransactionErrorMessage = (*Backend)(nil) 124 125 // New creates backend instance 126 func New(params Params) (*Backend, error) { 127 retry := newRetry(params.Log) 128 if params.RetryEnabled { 129 retry.Activate() 130 } 131 132 loggedScripts, err := lru.New[[md5.Size]byte, time.Time](DefaultLoggedScriptsCacheSize) 133 if err != nil { 134 return nil, fmt.Errorf("failed to initialize script logging cache: %w", err) 135 } 136 137 var txResCache *lru.Cache[flow.Identifier, *access.TransactionResult] 138 if params.TxResultCacheSize > 0 { 139 txResCache, err = lru.New[flow.Identifier, *access.TransactionResult](int(params.TxResultCacheSize)) 140 if err != nil { 141 return nil, fmt.Errorf("failed to init cache for transaction results: %w", err) 142 } 143 } 144 145 // NOTE: The transaction error message cache is currently only used by the access node and not by the observer node. 146 // To avoid introducing unnecessary command line arguments in the observer, one case could be that the error 147 // message cache is nil for the observer node. 148 var txErrorMessagesCache *lru.Cache[flow.Identifier, string] 149 150 if params.TxErrorMessagesCacheSize > 0 { 151 txErrorMessagesCache, err = lru.New[flow.Identifier, string](int(params.TxErrorMessagesCacheSize)) 152 if err != nil { 153 return nil, fmt.Errorf("failed to init cache for transaction error messages: %w", err) 154 } 155 } 156 157 // the system tx is hardcoded and never changes during runtime 158 systemTx, err := blueprints.SystemChunkTransaction(params.ChainID.Chain()) 159 if err != nil { 160 return nil, fmt.Errorf("failed to create system chunk transaction: %w", err) 161 } 162 systemTxID := systemTx.ID() 163 164 // initialize node version info 165 nodeInfo := getNodeVersionInfo(params.State.Params()) 166 167 transactionsLocalDataProvider := &TransactionsLocalDataProvider{ 168 state: params.State, 169 collections: params.Collections, 170 blocks: params.Blocks, 171 eventsIndex: params.EventsIndex, 172 txResultsIndex: params.TxResultsIndex, 173 systemTxID: systemTxID, 174 lastFullBlockHeight: params.LastFullBlockHeight, 175 } 176 177 b := &Backend{ 178 state: params.State, 179 BlockTracker: params.BlockTracker, 180 // create the sub-backends 181 backendScripts: backendScripts{ 182 log: params.Log, 183 headers: params.Headers, 184 executionReceipts: params.ExecutionReceipts, 185 connFactory: params.ConnFactory, 186 state: params.State, 187 metrics: params.AccessMetrics, 188 loggedScripts: loggedScripts, 189 nodeCommunicator: params.Communicator, 190 scriptExecutor: params.ScriptExecutor, 191 scriptExecMode: params.ScriptExecutionMode, 192 }, 193 backendEvents: backendEvents{ 194 log: params.Log, 195 chain: params.ChainID.Chain(), 196 state: params.State, 197 headers: params.Headers, 198 executionReceipts: params.ExecutionReceipts, 199 connFactory: params.ConnFactory, 200 maxHeightRange: params.MaxHeightRange, 201 nodeCommunicator: params.Communicator, 202 queryMode: params.EventQueryMode, 203 eventsIndex: params.EventsIndex, 204 }, 205 backendBlockHeaders: backendBlockHeaders{ 206 headers: params.Headers, 207 state: params.State, 208 }, 209 backendBlockDetails: backendBlockDetails{ 210 blocks: params.Blocks, 211 state: params.State, 212 }, 213 backendAccounts: backendAccounts{ 214 log: params.Log, 215 state: params.State, 216 headers: params.Headers, 217 executionReceipts: params.ExecutionReceipts, 218 connFactory: params.ConnFactory, 219 nodeCommunicator: params.Communicator, 220 scriptExecutor: params.ScriptExecutor, 221 scriptExecMode: params.ScriptExecutionMode, 222 }, 223 backendExecutionResults: backendExecutionResults{ 224 executionResults: params.ExecutionResults, 225 }, 226 backendNetwork: backendNetwork{ 227 state: params.State, 228 chainID: params.ChainID, 229 headers: params.Headers, 230 snapshotHistoryLimit: params.SnapshotHistoryLimit, 231 }, 232 backendSubscribeBlocks: backendSubscribeBlocks{ 233 log: params.Log, 234 state: params.State, 235 headers: params.Headers, 236 blocks: params.Blocks, 237 subscriptionHandler: params.SubscriptionHandler, 238 blockTracker: params.BlockTracker, 239 }, 240 241 collections: params.Collections, 242 executionReceipts: params.ExecutionReceipts, 243 connFactory: params.ConnFactory, 244 chainID: params.ChainID, 245 nodeInfo: nodeInfo, 246 } 247 248 b.backendTransactions = backendTransactions{ 249 TransactionsLocalDataProvider: transactionsLocalDataProvider, 250 log: params.Log, 251 staticCollectionRPC: params.CollectionRPC, 252 chainID: params.ChainID, 253 transactions: params.Transactions, 254 executionReceipts: params.ExecutionReceipts, 255 transactionValidator: configureTransactionValidator(params.State, params.ChainID), 256 transactionMetrics: params.AccessMetrics, 257 retry: retry, 258 connFactory: params.ConnFactory, 259 previousAccessNodes: params.HistoricalAccessNodes, 260 nodeCommunicator: params.Communicator, 261 txResultCache: txResCache, 262 txErrorMessagesCache: txErrorMessagesCache, 263 txResultQueryMode: params.TxResultQueryMode, 264 systemTx: systemTx, 265 systemTxID: systemTxID, 266 } 267 268 // TODO: The TransactionErrorMessage interface should be reorganized in future, as it is implemented in backendTransactions but used in TransactionsLocalDataProvider, and its initialization is somewhat quirky. 269 b.backendTransactions.txErrorMessages = b 270 271 b.backendSubscribeTransactions = backendSubscribeTransactions{ 272 txLocalDataProvider: transactionsLocalDataProvider, 273 backendTransactions: &b.backendTransactions, 274 log: params.Log, 275 executionResults: params.ExecutionResults, 276 subscriptionHandler: params.SubscriptionHandler, 277 blockTracker: params.BlockTracker, 278 } 279 280 retry.SetBackend(b) 281 282 preferredENIdentifiers, err = identifierList(params.PreferredExecutionNodeIDs) 283 if err != nil { 284 return nil, fmt.Errorf("failed to convert node id string to Flow Identifier for preferred EN map: %w", err) 285 } 286 287 fixedENIdentifiers, err = identifierList(params.FixedExecutionNodeIDs) 288 if err != nil { 289 return nil, fmt.Errorf("failed to convert node id string to Flow Identifier for fixed EN map: %w", err) 290 } 291 292 return b, nil 293 } 294 295 func identifierList(ids []string) (flow.IdentifierList, error) { 296 idList := make(flow.IdentifierList, len(ids)) 297 for i, idStr := range ids { 298 id, err := flow.HexStringToIdentifier(idStr) 299 if err != nil { 300 return nil, fmt.Errorf("failed to convert node id string %s to Flow Identifier: %w", id, err) 301 } 302 idList[i] = id 303 } 304 return idList, nil 305 } 306 307 func configureTransactionValidator(state protocol.State, chainID flow.ChainID) *access.TransactionValidator { 308 return access.NewTransactionValidator( 309 access.NewProtocolStateBlocks(state), 310 chainID.Chain(), 311 access.TransactionValidationOptions{ 312 Expiry: flow.DefaultTransactionExpiry, 313 ExpiryBuffer: flow.DefaultTransactionExpiryBuffer, 314 AllowEmptyReferenceBlockID: false, 315 AllowUnknownReferenceBlockID: false, 316 CheckScriptsParse: false, 317 MaxGasLimit: flow.DefaultMaxTransactionGasLimit, 318 MaxTransactionByteSize: flow.DefaultMaxTransactionByteSize, 319 MaxCollectionByteSize: flow.DefaultMaxCollectionByteSize, 320 }, 321 ) 322 } 323 324 // Ping responds to requests when the server is up. 325 func (b *Backend) Ping(ctx context.Context) error { 326 // staticCollectionRPC is only set if a collection node address was provided at startup 327 if b.staticCollectionRPC != nil { 328 _, err := b.staticCollectionRPC.Ping(ctx, &accessproto.PingRequest{}) 329 if err != nil { 330 return fmt.Errorf("could not ping collection node: %w", err) 331 } 332 } 333 334 return nil 335 } 336 337 // GetNodeVersionInfo returns node version information such as semver, commit, sporkID, protocolVersion, etc 338 func (b *Backend) GetNodeVersionInfo(_ context.Context) (*access.NodeVersionInfo, error) { 339 return b.nodeInfo, nil 340 } 341 342 // getNodeVersionInfo returns the NodeVersionInfo for the node. 343 // Since these values are static while the node is running, it is safe to cache. 344 func getNodeVersionInfo(stateParams protocol.Params) *access.NodeVersionInfo { 345 sporkID := stateParams.SporkID() 346 protocolVersion := stateParams.ProtocolVersion() 347 sporkRootBlockHeight := stateParams.SporkRootBlockHeight() 348 349 nodeRootBlockHeader := stateParams.SealedRoot() 350 351 nodeInfo := &access.NodeVersionInfo{ 352 Semver: build.Version(), 353 Commit: build.Commit(), 354 SporkId: sporkID, 355 ProtocolVersion: uint64(protocolVersion), 356 SporkRootBlockHeight: sporkRootBlockHeight, 357 NodeRootBlockHeight: nodeRootBlockHeader.Height, 358 } 359 360 return nodeInfo 361 } 362 363 func (b *Backend) GetCollectionByID(_ context.Context, colID flow.Identifier) (*flow.LightCollection, error) { 364 // retrieve the collection from the collection storage 365 col, err := b.collections.LightByID(colID) 366 if err != nil { 367 // Collections are retrieved asynchronously as we finalize blocks, so 368 // it is possible for a client to request a finalized block from us 369 // containing some collection, then get a not found error when requesting 370 // that collection. These clients should retry. 371 err = rpc.ConvertStorageError(fmt.Errorf("please retry for collection in finalized block: %w", err)) 372 return nil, err 373 } 374 375 return col, nil 376 } 377 378 func (b *Backend) GetNetworkParameters(_ context.Context) access.NetworkParameters { 379 return access.NetworkParameters{ 380 ChainID: b.chainID, 381 } 382 } 383 384 // executionNodesForBlockID returns upto maxNodesCnt number of randomly chosen execution node identities 385 // which have executed the given block ID. 386 // If no such execution node is found, an InsufficientExecutionReceipts error is returned. 387 func executionNodesForBlockID( 388 ctx context.Context, 389 blockID flow.Identifier, 390 executionReceipts storage.ExecutionReceipts, 391 state protocol.State, 392 log zerolog.Logger, 393 ) (flow.IdentitySkeletonList, error) { 394 var ( 395 executorIDs flow.IdentifierList 396 err error 397 ) 398 399 // check if the block ID is of the root block. If it is then don't look for execution receipts since they 400 // will not be present for the root block. 401 rootBlock := state.Params().FinalizedRoot() 402 403 if rootBlock.ID() == blockID { 404 executorIdentities, err := state.Final().Identities(filter.HasRole[flow.Identity](flow.RoleExecution)) 405 if err != nil { 406 return nil, fmt.Errorf("failed to retreive execution IDs for block ID %v: %w", blockID, err) 407 } 408 executorIDs = executorIdentities.NodeIDs() 409 } else { 410 // try to find atleast minExecutionNodesCnt execution node ids from the execution receipts for the given blockID 411 for attempt := 0; attempt < maxAttemptsForExecutionReceipt; attempt++ { 412 executorIDs, err = findAllExecutionNodes(blockID, executionReceipts, log) 413 if err != nil { 414 return nil, err 415 } 416 417 if len(executorIDs) >= minExecutionNodesCnt { 418 break 419 } 420 421 // log the attempt 422 log.Debug().Int("attempt", attempt).Int("max_attempt", maxAttemptsForExecutionReceipt). 423 Int("execution_receipts_found", len(executorIDs)). 424 Str("block_id", blockID.String()). 425 Msg("insufficient execution receipts") 426 427 // if one or less execution receipts may have been received then re-query 428 // in the hope that more might have been received by now 429 430 select { 431 case <-ctx.Done(): 432 return nil, ctx.Err() 433 case <-time.After(100 * time.Millisecond << time.Duration(attempt)): 434 // retry after an exponential backoff 435 } 436 } 437 438 receiptCnt := len(executorIDs) 439 // if less than minExecutionNodesCnt execution receipts have been received so far, then return random ENs 440 if receiptCnt < minExecutionNodesCnt { 441 newExecutorIDs, err := state.AtBlockID(blockID).Identities(filter.HasRole[flow.Identity](flow.RoleExecution)) 442 if err != nil { 443 return nil, fmt.Errorf("failed to retreive execution IDs for block ID %v: %w", blockID, err) 444 } 445 executorIDs = newExecutorIDs.NodeIDs() 446 } 447 } 448 449 // choose from the preferred or fixed execution nodes 450 subsetENs, err := chooseExecutionNodes(state, executorIDs) 451 if err != nil { 452 return nil, fmt.Errorf("failed to retreive execution IDs for block ID %v: %w", blockID, err) 453 } 454 455 if len(subsetENs) == 0 { 456 return nil, fmt.Errorf("no matching execution node found for block ID %v", blockID) 457 } 458 459 return subsetENs, nil 460 } 461 462 // findAllExecutionNodes find all the execution nodes ids from the execution receipts that have been received for the 463 // given blockID 464 func findAllExecutionNodes( 465 blockID flow.Identifier, 466 executionReceipts storage.ExecutionReceipts, 467 log zerolog.Logger, 468 ) (flow.IdentifierList, error) { 469 // lookup the receipt's storage with the block ID 470 allReceipts, err := executionReceipts.ByBlockID(blockID) 471 if err != nil { 472 return nil, fmt.Errorf("failed to retreive execution receipts for block ID %v: %w", blockID, err) 473 } 474 475 executionResultMetaList := make(flow.ExecutionReceiptMetaList, 0, len(allReceipts)) 476 for _, r := range allReceipts { 477 executionResultMetaList = append(executionResultMetaList, r.Meta()) 478 } 479 executionResultGroupedMetaList := executionResultMetaList.GroupByResultID() 480 481 // maximum number of matching receipts found so far for any execution result id 482 maxMatchedReceiptCnt := 0 483 // execution result id key for the highest number of matching receipts in the identicalReceipts map 484 var maxMatchedReceiptResultID flow.Identifier 485 486 // find the largest list of receipts which have the same result ID 487 for resultID, executionReceiptList := range executionResultGroupedMetaList { 488 currentMatchedReceiptCnt := executionReceiptList.Size() 489 if currentMatchedReceiptCnt > maxMatchedReceiptCnt { 490 maxMatchedReceiptCnt = currentMatchedReceiptCnt 491 maxMatchedReceiptResultID = resultID 492 } 493 } 494 495 // if there are more than one execution result for the same block ID, log as error 496 if executionResultGroupedMetaList.NumberGroups() > 1 { 497 identicalReceiptsStr := fmt.Sprintf("%v", flow.GetIDs(allReceipts)) 498 log.Error(). 499 Str("block_id", blockID.String()). 500 Str("execution_receipts", identicalReceiptsStr). 501 Msg("execution receipt mismatch") 502 } 503 504 // pick the largest list of matching receipts 505 matchingReceiptMetaList := executionResultGroupedMetaList.GetGroup(maxMatchedReceiptResultID) 506 507 metaReceiptGroupedByExecutorID := matchingReceiptMetaList.GroupByExecutorID() 508 509 // collect all unique execution node ids from the receipts 510 var executorIDs flow.IdentifierList 511 for executorID := range metaReceiptGroupedByExecutorID { 512 executorIDs = append(executorIDs, executorID) 513 } 514 515 return executorIDs, nil 516 } 517 518 // chooseExecutionNodes finds the subset of execution nodes defined in the identity table by first 519 // choosing the preferred execution nodes which have executed the transaction. If no such preferred 520 // execution nodes are found, then the fixed execution nodes defined in the identity table are returned 521 // If neither preferred nor fixed nodes are defined, then all execution node matching the executor IDs are returned. 522 // e.g. If execution nodes in identity table are {1,2,3,4}, preferred ENs are defined as {2,3,4} 523 // and the executor IDs is {1,2,3}, then {2, 3} is returned as the chosen subset of ENs 524 func chooseExecutionNodes(state protocol.State, executorIDs flow.IdentifierList) (flow.IdentitySkeletonList, error) { 525 allENs, err := state.Final().Identities(filter.HasRole[flow.Identity](flow.RoleExecution)) 526 if err != nil { 527 return nil, fmt.Errorf("failed to retreive all execution IDs: %w", err) 528 } 529 530 // first try and choose from the preferred EN IDs 531 var chosenIDs flow.IdentityList 532 if len(preferredENIdentifiers) > 0 { 533 // find the preferred execution node IDs which have executed the transaction 534 chosenIDs = allENs.Filter(filter.And(filter.HasNodeID[flow.Identity](preferredENIdentifiers...), 535 filter.HasNodeID[flow.Identity](executorIDs...))) 536 if len(chosenIDs) > 0 { 537 return chosenIDs.ToSkeleton(), nil 538 } 539 } 540 541 // if no preferred EN ID is found, then choose from the fixed EN IDs 542 if len(fixedENIdentifiers) > 0 { 543 // choose fixed ENs which have executed the transaction 544 chosenIDs = allENs.Filter(filter.And( 545 filter.HasNodeID[flow.Identity](fixedENIdentifiers...), 546 filter.HasNodeID[flow.Identity](executorIDs...))) 547 if len(chosenIDs) > 0 { 548 return chosenIDs.ToSkeleton(), nil 549 } 550 // if no such ENs are found then just choose all fixed ENs 551 chosenIDs = allENs.Filter(filter.HasNodeID[flow.Identity](fixedENIdentifiers...)) 552 return chosenIDs.ToSkeleton(), nil 553 } 554 555 // If no preferred or fixed ENs have been specified, then return all executor IDs i.e. no preference at all 556 return allENs.Filter(filter.HasNodeID[flow.Identity](executorIDs...)).ToSkeleton(), nil 557 }