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