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

     1  package backend
     2  
     3  import (
     4  	"context"
     5  	"crypto/md5" //nolint:gosec
     6  	"fmt"
     7  	"testing"
     8  	"time"
     9  
    10  	lru "github.com/hashicorp/golang-lru/v2"
    11  	"github.com/rs/zerolog"
    12  	"github.com/stretchr/testify/mock"
    13  	"github.com/stretchr/testify/suite"
    14  	"google.golang.org/grpc/codes"
    15  	"google.golang.org/grpc/status"
    16  
    17  	execproto "github.com/onflow/flow/protobuf/go/flow/execution"
    18  
    19  	access "github.com/onflow/flow-go/engine/access/mock"
    20  	connectionmock "github.com/onflow/flow-go/engine/access/rpc/connection/mock"
    21  	fvmerrors "github.com/onflow/flow-go/fvm/errors"
    22  	"github.com/onflow/flow-go/model/flow"
    23  	execmock "github.com/onflow/flow-go/module/execution/mock"
    24  	"github.com/onflow/flow-go/module/irrecoverable"
    25  	"github.com/onflow/flow-go/module/metrics"
    26  	protocol "github.com/onflow/flow-go/state/protocol/mock"
    27  	"github.com/onflow/flow-go/storage"
    28  	storagemock "github.com/onflow/flow-go/storage/mock"
    29  	"github.com/onflow/flow-go/utils/unittest"
    30  )
    31  
    32  var (
    33  	expectedResponse = []byte("response_data")
    34  
    35  	cadenceErr    = fvmerrors.NewCodedError(fvmerrors.ErrCodeCadenceRunTimeError, "cadence error")
    36  	fvmFailureErr = fvmerrors.NewCodedFailure(fvmerrors.FailureCodeBlockFinderFailure, "fvm error")
    37  	ctxCancelErr  = fvmerrors.NewCodedError(fvmerrors.ErrCodeScriptExecutionCancelledError, "context canceled error")
    38  	timeoutErr    = fvmerrors.NewCodedError(fvmerrors.ErrCodeScriptExecutionTimedOutError, "timeout error")
    39  	compLimitErr  = fvmerrors.NewCodedError(fvmerrors.ErrCodeComputationLimitExceededError, "computation limit exceeded error")
    40  	memLimitErr   = fvmerrors.NewCodedError(fvmerrors.ErrCodeMemoryLimitExceededError, "memory limit exceeded error")
    41  )
    42  
    43  // Create a suite similar to GetAccount that covers each of the modes
    44  type BackendScriptsSuite struct {
    45  	suite.Suite
    46  
    47  	log        zerolog.Logger
    48  	state      *protocol.State
    49  	snapshot   *protocol.Snapshot
    50  	params     *protocol.Params
    51  	rootHeader *flow.Header
    52  
    53  	headers           *storagemock.Headers
    54  	receipts          *storagemock.ExecutionReceipts
    55  	connectionFactory *connectionmock.ConnectionFactory
    56  	chainID           flow.ChainID
    57  
    58  	executionNodes flow.IdentityList
    59  	execClient     *access.ExecutionAPIClient
    60  
    61  	block *flow.Block
    62  
    63  	script        []byte
    64  	arguments     [][]byte
    65  	failingScript []byte
    66  }
    67  
    68  func TestBackendScriptsSuite(t *testing.T) {
    69  	suite.Run(t, new(BackendScriptsSuite))
    70  }
    71  
    72  func (s *BackendScriptsSuite) SetupTest() {
    73  	s.log = unittest.Logger()
    74  	s.state = protocol.NewState(s.T())
    75  	s.snapshot = protocol.NewSnapshot(s.T())
    76  	s.rootHeader = unittest.BlockHeaderFixture()
    77  	s.params = protocol.NewParams(s.T())
    78  	s.headers = storagemock.NewHeaders(s.T())
    79  	s.receipts = storagemock.NewExecutionReceipts(s.T())
    80  	s.connectionFactory = connectionmock.NewConnectionFactory(s.T())
    81  	s.chainID = flow.Testnet
    82  
    83  	s.execClient = access.NewExecutionAPIClient(s.T())
    84  	s.executionNodes = unittest.IdentityListFixture(2, unittest.WithRole(flow.RoleExecution))
    85  
    86  	block := unittest.BlockFixture()
    87  	s.block = &block
    88  
    89  	s.script = []byte("access(all) fun main() { return 1 }")
    90  	s.arguments = [][]byte{[]byte("arg1"), []byte("arg2")}
    91  	s.failingScript = []byte("access(all) fun main() { panic(\"!!\") }")
    92  }
    93  
    94  func (s *BackendScriptsSuite) defaultBackend() *backendScripts {
    95  	loggedScripts, err := lru.New[[md5.Size]byte, time.Time](DefaultLoggedScriptsCacheSize)
    96  	s.Require().NoError(err)
    97  
    98  	return &backendScripts{
    99  		log:               s.log,
   100  		metrics:           metrics.NewNoopCollector(),
   101  		state:             s.state,
   102  		headers:           s.headers,
   103  		executionReceipts: s.receipts,
   104  		loggedScripts:     loggedScripts,
   105  		connFactory:       s.connectionFactory,
   106  		nodeCommunicator:  NewNodeCommunicator(false),
   107  	}
   108  }
   109  
   110  // setupExecutionNodes sets up the mocks required to test against an EN backend
   111  func (s *BackendScriptsSuite) setupExecutionNodes(block *flow.Block) {
   112  	s.params.On("FinalizedRoot").Return(s.rootHeader, nil)
   113  	s.state.On("Params").Return(s.params)
   114  	s.state.On("Final").Return(s.snapshot)
   115  	s.snapshot.On("Identities", mock.Anything).Return(s.executionNodes, nil)
   116  
   117  	// this line causes a S1021 lint error because receipts is explicitly declared. this is required
   118  	// to ensure the mock library handles the response type correctly
   119  	var receipts flow.ExecutionReceiptList //nolint:gosimple
   120  	receipts = unittest.ReceiptsForBlockFixture(block, s.executionNodes.NodeIDs())
   121  	s.receipts.On("ByBlockID", block.ID()).Return(receipts, nil)
   122  
   123  	s.connectionFactory.On("GetExecutionAPIClient", mock.Anything).
   124  		Return(s.execClient, &mockCloser{}, nil)
   125  }
   126  
   127  // setupENSuccessResponse configures the execution client mock to return a successful response
   128  func (s *BackendScriptsSuite) setupENSuccessResponse(blockID flow.Identifier) {
   129  	expectedExecRequest := &execproto.ExecuteScriptAtBlockIDRequest{
   130  		BlockId:   blockID[:],
   131  		Script:    s.script,
   132  		Arguments: s.arguments,
   133  	}
   134  
   135  	s.execClient.On("ExecuteScriptAtBlockID", mock.Anything, expectedExecRequest).
   136  		Return(&execproto.ExecuteScriptAtBlockIDResponse{
   137  			Value: expectedResponse,
   138  		}, nil)
   139  }
   140  
   141  // setupENFailingResponse configures the execution client mock to return a failing response
   142  func (s *BackendScriptsSuite) setupENFailingResponse(blockID flow.Identifier, err error) {
   143  	expectedExecRequest := &execproto.ExecuteScriptAtBlockIDRequest{
   144  		BlockId:   blockID[:],
   145  		Script:    s.failingScript,
   146  		Arguments: s.arguments,
   147  	}
   148  
   149  	s.execClient.On("ExecuteScriptAtBlockID", mock.Anything, expectedExecRequest).
   150  		Return(nil, err)
   151  }
   152  
   153  // TestExecuteScriptOnExecutionNode_HappyPath tests that the backend successfully executes scripts
   154  // on execution nodes
   155  func (s *BackendScriptsSuite) TestExecuteScriptOnExecutionNode_HappyPath() {
   156  	ctx := context.Background()
   157  
   158  	s.setupExecutionNodes(s.block)
   159  	s.setupENSuccessResponse(s.block.ID())
   160  
   161  	backend := s.defaultBackend()
   162  	backend.scriptExecMode = IndexQueryModeExecutionNodesOnly
   163  
   164  	s.Run("GetAccount", func() {
   165  		s.testExecuteScriptAtLatestBlock(ctx, backend, codes.OK)
   166  	})
   167  
   168  	s.Run("ExecuteScriptAtBlockID", func() {
   169  		s.testExecuteScriptAtBlockID(ctx, backend, codes.OK)
   170  	})
   171  
   172  	s.Run("ExecuteScriptAtBlockHeight", func() {
   173  		s.testExecuteScriptAtBlockHeight(ctx, backend, codes.OK)
   174  	})
   175  }
   176  
   177  // TestExecuteScriptOnExecutionNode_Fails tests that the backend returns an error when the execution
   178  // node returns an error
   179  func (s *BackendScriptsSuite) TestExecuteScriptOnExecutionNode_Fails() {
   180  	ctx := context.Background()
   181  
   182  	// use a status code that's not used in the API to make sure it's passed through
   183  	statusCode := codes.FailedPrecondition
   184  	errToReturn := status.Error(statusCode, "random error")
   185  
   186  	s.setupExecutionNodes(s.block)
   187  	s.setupENFailingResponse(s.block.ID(), errToReturn)
   188  
   189  	backend := s.defaultBackend()
   190  	backend.scriptExecMode = IndexQueryModeExecutionNodesOnly
   191  
   192  	s.Run("GetAccount", func() {
   193  		s.testExecuteScriptAtLatestBlock(ctx, backend, statusCode)
   194  	})
   195  
   196  	s.Run("ExecuteScriptAtBlockID", func() {
   197  		s.testExecuteScriptAtBlockID(ctx, backend, statusCode)
   198  	})
   199  
   200  	s.Run("ExecuteScriptAtBlockHeight", func() {
   201  		s.testExecuteScriptAtBlockHeight(ctx, backend, statusCode)
   202  	})
   203  }
   204  
   205  // TestExecuteScriptFromStorage_HappyPath tests that the backend successfully executes scripts using
   206  // the local storage
   207  func (s *BackendScriptsSuite) TestExecuteScriptFromStorage_HappyPath() {
   208  	ctx := context.Background()
   209  
   210  	scriptExecutor := execmock.NewScriptExecutor(s.T())
   211  	scriptExecutor.On("ExecuteAtBlockHeight", mock.Anything, s.script, s.arguments, s.block.Header.Height).
   212  		Return(expectedResponse, nil)
   213  
   214  	backend := s.defaultBackend()
   215  	backend.scriptExecMode = IndexQueryModeLocalOnly
   216  	backend.scriptExecutor = scriptExecutor
   217  
   218  	s.Run("GetAccount - happy path", func() {
   219  		s.testExecuteScriptAtLatestBlock(ctx, backend, codes.OK)
   220  	})
   221  
   222  	s.Run("GetAccountAtLatestBlock - happy path", func() {
   223  		s.testExecuteScriptAtBlockID(ctx, backend, codes.OK)
   224  	})
   225  
   226  	s.Run("GetAccountAtBlockHeight - happy path", func() {
   227  		s.testExecuteScriptAtBlockHeight(ctx, backend, codes.OK)
   228  	})
   229  }
   230  
   231  // TestExecuteScriptFromStorage_Fails tests that errors received from local storage are handled
   232  // and converted to the appropriate status code
   233  func (s *BackendScriptsSuite) TestExecuteScriptFromStorage_Fails() {
   234  	ctx := context.Background()
   235  
   236  	scriptExecutor := execmock.NewScriptExecutor(s.T())
   237  
   238  	backend := s.defaultBackend()
   239  	backend.scriptExecMode = IndexQueryModeLocalOnly
   240  	backend.scriptExecutor = scriptExecutor
   241  
   242  	testCases := []struct {
   243  		err        error
   244  		statusCode codes.Code
   245  	}{
   246  		{
   247  			err:        storage.ErrHeightNotIndexed,
   248  			statusCode: codes.OutOfRange,
   249  		},
   250  		{
   251  			err:        storage.ErrNotFound,
   252  			statusCode: codes.NotFound,
   253  		},
   254  		{
   255  			err:        fmt.Errorf("system error"),
   256  			statusCode: codes.Internal,
   257  		},
   258  		{
   259  			err:        cadenceErr,
   260  			statusCode: codes.InvalidArgument,
   261  		},
   262  		{
   263  			err:        fvmFailureErr,
   264  			statusCode: codes.Internal,
   265  		},
   266  	}
   267  
   268  	for _, tt := range testCases {
   269  		scriptExecutor.On("ExecuteAtBlockHeight", mock.Anything, s.failingScript, s.arguments, s.block.Header.Height).
   270  			Return(nil, tt.err).Times(3)
   271  
   272  		s.Run(fmt.Sprintf("GetAccount - fails with %v", tt.err), func() {
   273  			s.testExecuteScriptAtLatestBlock(ctx, backend, tt.statusCode)
   274  		})
   275  
   276  		s.Run(fmt.Sprintf("GetAccountAtLatestBlock - fails with %v", tt.err), func() {
   277  			s.testExecuteScriptAtBlockID(ctx, backend, tt.statusCode)
   278  		})
   279  
   280  		s.Run(fmt.Sprintf("GetAccountAtBlockHeight - fails with %v", tt.err), func() {
   281  			s.testExecuteScriptAtBlockHeight(ctx, backend, tt.statusCode)
   282  		})
   283  	}
   284  }
   285  
   286  // TestExecuteScriptWithFailover_HappyPath tests that when an error is returned executing a script
   287  // from local storage, the backend will attempt to run it on an execution node
   288  func (s *BackendScriptsSuite) TestExecuteScriptWithFailover_HappyPath() {
   289  	ctx := context.Background()
   290  
   291  	errors := []error{
   292  		storage.ErrHeightNotIndexed,
   293  		storage.ErrNotFound,
   294  		fmt.Errorf("system error"),
   295  		fvmFailureErr,
   296  		compLimitErr,
   297  		memLimitErr,
   298  	}
   299  
   300  	s.setupExecutionNodes(s.block)
   301  	s.setupENSuccessResponse(s.block.ID())
   302  
   303  	scriptExecutor := execmock.NewScriptExecutor(s.T())
   304  
   305  	backend := s.defaultBackend()
   306  	backend.scriptExecMode = IndexQueryModeFailover
   307  	backend.scriptExecutor = scriptExecutor
   308  
   309  	for _, errToReturn := range errors {
   310  		// configure local script executor to fail
   311  		scriptExecutor.On("ExecuteAtBlockHeight", mock.Anything, s.script, s.arguments, s.block.Header.Height).
   312  			Return(nil, errToReturn).Times(3)
   313  
   314  		s.Run(fmt.Sprintf("ExecuteScriptAtLatestBlock - recovers %v", errToReturn), func() {
   315  			s.testExecuteScriptAtLatestBlock(ctx, backend, codes.OK)
   316  		})
   317  
   318  		s.Run(fmt.Sprintf("ExecuteScriptAtBlockID - recovers %v", errToReturn), func() {
   319  			s.testExecuteScriptAtBlockID(ctx, backend, codes.OK)
   320  		})
   321  
   322  		s.Run(fmt.Sprintf("ExecuteScriptAtBlockHeight - recovers %v", errToReturn), func() {
   323  			s.testExecuteScriptAtBlockHeight(ctx, backend, codes.OK)
   324  		})
   325  	}
   326  }
   327  
   328  // TestExecuteScriptWithFailover_SkippedForCorrectCodes tests that failover is skipped for
   329  // FVM errors that result in InvalidArgument or Canceled errors
   330  func (s *BackendScriptsSuite) TestExecuteScriptWithFailover_SkippedForCorrectCodes() {
   331  	ctx := context.Background()
   332  
   333  	// configure local script executor to fail
   334  	scriptExecutor := execmock.NewScriptExecutor(s.T())
   335  
   336  	backend := s.defaultBackend()
   337  	backend.scriptExecMode = IndexQueryModeFailover
   338  	backend.scriptExecutor = scriptExecutor
   339  
   340  	testCases := []struct {
   341  		err        error
   342  		statusCode codes.Code
   343  	}{
   344  		{
   345  			err:        cadenceErr,
   346  			statusCode: codes.InvalidArgument,
   347  		},
   348  		{
   349  			err:        ctxCancelErr,
   350  			statusCode: codes.Canceled,
   351  		},
   352  	}
   353  
   354  	for _, tt := range testCases {
   355  		scriptExecutor.On("ExecuteAtBlockHeight", mock.Anything, s.failingScript, s.arguments, s.block.Header.Height).
   356  			Return(nil, tt.err).
   357  			Times(3)
   358  
   359  		s.Run(fmt.Sprintf("ExecuteScriptAtLatestBlock - %s", tt.statusCode), func() {
   360  			s.testExecuteScriptAtLatestBlock(ctx, backend, tt.statusCode)
   361  		})
   362  
   363  		s.Run(fmt.Sprintf("ExecuteScriptAtBlockID - %s", tt.statusCode), func() {
   364  			s.testExecuteScriptAtBlockID(ctx, backend, tt.statusCode)
   365  		})
   366  
   367  		s.Run(fmt.Sprintf("ExecuteScriptAtBlockHeight - %s", tt.statusCode), func() {
   368  			s.testExecuteScriptAtBlockHeight(ctx, backend, tt.statusCode)
   369  		})
   370  	}
   371  }
   372  
   373  // TestExecuteScriptWithFailover_ReturnsENErrors tests that when an error is returned from the execution
   374  // node during a failover, it is returned to the caller.
   375  func (s *BackendScriptsSuite) TestExecuteScriptWithFailover_ReturnsENErrors() {
   376  	ctx := context.Background()
   377  
   378  	// use a status code that's not used in the API to make sure it's passed through
   379  	statusCode := codes.FailedPrecondition
   380  	errToReturn := status.Error(statusCode, "random error")
   381  
   382  	// setup the execution client mocks
   383  	s.setupExecutionNodes(s.block)
   384  	s.setupENFailingResponse(s.block.ID(), errToReturn)
   385  
   386  	// configure local script executor to fail
   387  	scriptExecutor := execmock.NewScriptExecutor(s.T())
   388  	scriptExecutor.On("ExecuteAtBlockHeight", mock.Anything, mock.Anything, mock.Anything, s.block.Header.Height).
   389  		Return(nil, storage.ErrHeightNotIndexed)
   390  
   391  	backend := s.defaultBackend()
   392  	backend.scriptExecMode = IndexQueryModeFailover
   393  	backend.scriptExecutor = scriptExecutor
   394  
   395  	s.Run("ExecuteScriptAtLatestBlock", func() {
   396  		s.testExecuteScriptAtLatestBlock(ctx, backend, statusCode)
   397  	})
   398  
   399  	s.Run("ExecuteScriptAtBlockID", func() {
   400  		s.testExecuteScriptAtBlockID(ctx, backend, statusCode)
   401  	})
   402  
   403  	s.Run("ExecuteScriptAtBlockHeight", func() {
   404  		s.testExecuteScriptAtBlockHeight(ctx, backend, statusCode)
   405  	})
   406  }
   407  
   408  // TestExecuteScriptAtLatestBlockFromStorage_InconsistentState tests that signaler context received error when node state is
   409  // inconsistent
   410  func (s *BackendScriptsSuite) TestExecuteScriptAtLatestBlockFromStorage_InconsistentState() {
   411  	scriptExecutor := execmock.NewScriptExecutor(s.T())
   412  
   413  	backend := s.defaultBackend()
   414  	backend.scriptExecMode = IndexQueryModeLocalOnly
   415  	backend.scriptExecutor = scriptExecutor
   416  
   417  	s.Run(fmt.Sprintf("ExecuteScriptAtLatestBlock - fails with %v", "inconsistent node's state"), func() {
   418  		s.state.On("Sealed").Return(s.snapshot, nil)
   419  
   420  		err := fmt.Errorf("inconsistent node's state")
   421  		s.snapshot.On("Head").Return(nil, err)
   422  
   423  		signCtxErr := irrecoverable.NewExceptionf("failed to lookup sealed header: %w", err)
   424  		signalerCtx := irrecoverable.WithSignalerContext(context.Background(),
   425  			irrecoverable.NewMockSignalerContextExpectError(s.T(), context.Background(), signCtxErr))
   426  
   427  		actual, err := backend.ExecuteScriptAtLatestBlock(signalerCtx, s.script, s.arguments)
   428  		s.Require().Error(err)
   429  		s.Require().Nil(actual)
   430  	})
   431  }
   432  
   433  func (s *BackendScriptsSuite) testExecuteScriptAtLatestBlock(ctx context.Context, backend *backendScripts, statusCode codes.Code) {
   434  	s.state.On("Sealed").Return(s.snapshot, nil).Once()
   435  	s.snapshot.On("Head").Return(s.block.Header, nil).Once()
   436  
   437  	if statusCode == codes.OK {
   438  		actual, err := backend.ExecuteScriptAtLatestBlock(ctx, s.script, s.arguments)
   439  		s.Require().NoError(err)
   440  		s.Require().Equal(expectedResponse, actual)
   441  	} else {
   442  		actual, err := backend.ExecuteScriptAtLatestBlock(ctx, s.failingScript, s.arguments)
   443  		s.Require().Error(err)
   444  		s.Require().Equal(statusCode, status.Code(err), "error code mismatch: expected %d, got %d: %s", statusCode, status.Code(err), err)
   445  		s.Require().Nil(actual)
   446  	}
   447  }
   448  
   449  func (s *BackendScriptsSuite) testExecuteScriptAtBlockID(ctx context.Context, backend *backendScripts, statusCode codes.Code) {
   450  	blockID := s.block.ID()
   451  	s.headers.On("ByBlockID", blockID).Return(s.block.Header, nil).Once()
   452  
   453  	if statusCode == codes.OK {
   454  		actual, err := backend.ExecuteScriptAtBlockID(ctx, blockID, s.script, s.arguments)
   455  		s.Require().NoError(err)
   456  		s.Require().Equal(expectedResponse, actual)
   457  	} else {
   458  		actual, err := backend.ExecuteScriptAtBlockID(ctx, blockID, s.failingScript, s.arguments)
   459  		s.Require().Error(err)
   460  		s.Require().Equal(statusCode, status.Code(err), "error code mismatch: expected %d, got %d: %s", statusCode, status.Code(err), err)
   461  		s.Require().Nil(actual)
   462  	}
   463  }
   464  
   465  func (s *BackendScriptsSuite) testExecuteScriptAtBlockHeight(ctx context.Context, backend *backendScripts, statusCode codes.Code) {
   466  	height := s.block.Header.Height
   467  	s.headers.On("ByHeight", height).Return(s.block.Header, nil).Once()
   468  
   469  	if statusCode == codes.OK {
   470  		actual, err := backend.ExecuteScriptAtBlockHeight(ctx, height, s.script, s.arguments)
   471  		s.Require().NoError(err)
   472  		s.Require().Equal(expectedResponse, actual)
   473  	} else {
   474  		actual, err := backend.ExecuteScriptAtBlockHeight(ctx, height, s.failingScript, s.arguments)
   475  		s.Require().Error(err)
   476  		s.Require().Equalf(statusCode, status.Code(err), "error code mismatch: expected %d, got %d: %s", statusCode, status.Code(err), err)
   477  		s.Require().Nil(actual)
   478  	}
   479  }