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  }