github.com/onflow/flow-go@v0.33.17/engine/access/rpc/backend/backend_accounts_test.go (about)

     1  package backend
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  
     8  	"github.com/rs/zerolog"
     9  	"github.com/stretchr/testify/mock"
    10  	"github.com/stretchr/testify/suite"
    11  	"google.golang.org/grpc/codes"
    12  	"google.golang.org/grpc/status"
    13  
    14  	access "github.com/onflow/flow-go/engine/access/mock"
    15  	connectionmock "github.com/onflow/flow-go/engine/access/rpc/connection/mock"
    16  	"github.com/onflow/flow-go/engine/common/rpc/convert"
    17  	"github.com/onflow/flow-go/model/flow"
    18  	execmock "github.com/onflow/flow-go/module/execution/mock"
    19  	"github.com/onflow/flow-go/module/irrecoverable"
    20  	protocol "github.com/onflow/flow-go/state/protocol/mock"
    21  	"github.com/onflow/flow-go/storage"
    22  	storagemock "github.com/onflow/flow-go/storage/mock"
    23  	"github.com/onflow/flow-go/utils/unittest"
    24  
    25  	execproto "github.com/onflow/flow/protobuf/go/flow/execution"
    26  )
    27  
    28  type BackendAccountsSuite struct {
    29  	suite.Suite
    30  
    31  	log        zerolog.Logger
    32  	state      *protocol.State
    33  	snapshot   *protocol.Snapshot
    34  	params     *protocol.Params
    35  	rootHeader *flow.Header
    36  
    37  	headers           *storagemock.Headers
    38  	receipts          *storagemock.ExecutionReceipts
    39  	connectionFactory *connectionmock.ConnectionFactory
    40  	chainID           flow.ChainID
    41  
    42  	executionNodes flow.IdentityList
    43  	execClient     *access.ExecutionAPIClient
    44  
    45  	block          *flow.Block
    46  	account        *flow.Account
    47  	failingAddress flow.Address
    48  }
    49  
    50  func TestBackendAccountsSuite(t *testing.T) {
    51  	suite.Run(t, new(BackendAccountsSuite))
    52  }
    53  
    54  func (s *BackendAccountsSuite) SetupTest() {
    55  	s.log = unittest.Logger()
    56  	s.state = protocol.NewState(s.T())
    57  	s.snapshot = protocol.NewSnapshot(s.T())
    58  	s.rootHeader = unittest.BlockHeaderFixture()
    59  	s.params = protocol.NewParams(s.T())
    60  	s.headers = storagemock.NewHeaders(s.T())
    61  	s.receipts = storagemock.NewExecutionReceipts(s.T())
    62  	s.connectionFactory = connectionmock.NewConnectionFactory(s.T())
    63  	s.chainID = flow.Testnet
    64  
    65  	s.execClient = access.NewExecutionAPIClient(s.T())
    66  	s.executionNodes = unittest.IdentityListFixture(2, unittest.WithRole(flow.RoleExecution))
    67  
    68  	block := unittest.BlockFixture()
    69  	s.block = &block
    70  
    71  	var err error
    72  	s.account, err = unittest.AccountFixture()
    73  	s.Require().NoError(err)
    74  
    75  	s.failingAddress = unittest.AddressFixture()
    76  }
    77  
    78  func (s *BackendAccountsSuite) defaultBackend() *backendAccounts {
    79  	return &backendAccounts{
    80  		log:               s.log,
    81  		state:             s.state,
    82  		headers:           s.headers,
    83  		executionReceipts: s.receipts,
    84  		connFactory:       s.connectionFactory,
    85  		nodeCommunicator:  NewNodeCommunicator(false),
    86  	}
    87  }
    88  
    89  // setupExecutionNodes sets up the mocks required to test against an EN backend
    90  func (s *BackendAccountsSuite) setupExecutionNodes(block *flow.Block) {
    91  	s.params.On("FinalizedRoot").Return(s.rootHeader, nil)
    92  	s.state.On("Params").Return(s.params)
    93  	s.state.On("Final").Return(s.snapshot)
    94  	s.snapshot.On("Identities", mock.Anything).Return(s.executionNodes, nil)
    95  
    96  	// this line causes a S1021 lint error because receipts is explicitly declared. this is required
    97  	// to ensure the mock library handles the response type correctly
    98  	var receipts flow.ExecutionReceiptList //nolint:gosimple
    99  	receipts = unittest.ReceiptsForBlockFixture(block, s.executionNodes.NodeIDs())
   100  	s.receipts.On("ByBlockID", block.ID()).Return(receipts, nil)
   101  
   102  	s.connectionFactory.On("GetExecutionAPIClient", mock.Anything).
   103  		Return(s.execClient, &mockCloser{}, nil)
   104  }
   105  
   106  // setupENSuccessResponse configures the execution node client to return a successful response
   107  func (s *BackendAccountsSuite) setupENSuccessResponse(blockID flow.Identifier) {
   108  	expectedExecRequest := &execproto.GetAccountAtBlockIDRequest{
   109  		BlockId: blockID[:],
   110  		Address: s.account.Address.Bytes(),
   111  	}
   112  
   113  	convertedAccount, err := convert.AccountToMessage(s.account)
   114  	s.Require().NoError(err)
   115  
   116  	s.execClient.On("GetAccountAtBlockID", mock.Anything, expectedExecRequest).
   117  		Return(&execproto.GetAccountAtBlockIDResponse{
   118  			Account: convertedAccount,
   119  		}, nil)
   120  }
   121  
   122  // setupENFailingResponse configures the execution node client to return an error
   123  func (s *BackendAccountsSuite) setupENFailingResponse(blockID flow.Identifier, err error) {
   124  	failingRequest := &execproto.GetAccountAtBlockIDRequest{
   125  		BlockId: blockID[:],
   126  		Address: s.failingAddress.Bytes(),
   127  	}
   128  
   129  	s.execClient.On("GetAccountAtBlockID", mock.Anything, failingRequest).
   130  		Return(nil, err)
   131  }
   132  
   133  // TestGetAccountFromExecutionNode_HappyPath tests successfully getting accounts from execution nodes
   134  func (s *BackendAccountsSuite) TestGetAccountFromExecutionNode_HappyPath() {
   135  	ctx := context.Background()
   136  
   137  	s.setupExecutionNodes(s.block)
   138  	s.setupENSuccessResponse(s.block.ID())
   139  
   140  	backend := s.defaultBackend()
   141  	backend.scriptExecMode = IndexQueryModeExecutionNodesOnly
   142  
   143  	s.Run("GetAccount - happy path", func() {
   144  		s.testGetAccount(ctx, backend, codes.OK)
   145  	})
   146  
   147  	s.Run("GetAccountAtLatestBlock - happy path", func() {
   148  		s.testGetAccountAtLatestBlock(ctx, backend, codes.OK)
   149  	})
   150  
   151  	s.Run("GetAccountAtBlockHeight - happy path", func() {
   152  		s.testGetAccountAtBlockHeight(ctx, backend, codes.OK)
   153  	})
   154  }
   155  
   156  // TestGetAccountFromExecutionNode_Fails errors received from execution nodes are returned
   157  func (s *BackendAccountsSuite) TestGetAccountFromExecutionNode_Fails() {
   158  	ctx := context.Background()
   159  
   160  	// use a status code that's not used in the API to make sure it's passed through
   161  	statusCode := codes.FailedPrecondition
   162  	errToReturn := status.Error(statusCode, "random error")
   163  
   164  	s.setupExecutionNodes(s.block)
   165  	s.setupENFailingResponse(s.block.ID(), errToReturn)
   166  
   167  	backend := s.defaultBackend()
   168  	backend.scriptExecMode = IndexQueryModeExecutionNodesOnly
   169  
   170  	s.Run("GetAccount - fails with backend err", func() {
   171  		s.testGetAccount(ctx, backend, statusCode)
   172  	})
   173  
   174  	s.Run("GetAccountAtLatestBlock - fails with backend err", func() {
   175  		s.testGetAccountAtLatestBlock(ctx, backend, statusCode)
   176  	})
   177  
   178  	s.Run("GetAccountAtBlockHeight - fails with backend err", func() {
   179  		s.testGetAccountAtBlockHeight(ctx, backend, statusCode)
   180  	})
   181  }
   182  
   183  // TestGetAccountFromStorage_HappyPath test successfully getting accounts from local storage
   184  func (s *BackendAccountsSuite) TestGetAccountFromStorage_HappyPath() {
   185  	ctx := context.Background()
   186  
   187  	scriptExecutor := execmock.NewScriptExecutor(s.T())
   188  	scriptExecutor.On("GetAccountAtBlockHeight", mock.Anything, s.account.Address, s.block.Header.Height).
   189  		Return(s.account, nil)
   190  
   191  	backend := s.defaultBackend()
   192  	backend.scriptExecMode = IndexQueryModeLocalOnly
   193  	backend.scriptExecutor = scriptExecutor
   194  
   195  	s.Run("GetAccount - happy path", func() {
   196  		s.testGetAccount(ctx, backend, codes.OK)
   197  	})
   198  
   199  	s.Run("GetAccountAtLatestBlock - happy path", func() {
   200  		s.testGetAccountAtLatestBlock(ctx, backend, codes.OK)
   201  	})
   202  
   203  	s.Run("GetAccountAtBlockHeight - happy path", func() {
   204  		s.testGetAccountAtBlockHeight(ctx, backend, codes.OK)
   205  	})
   206  }
   207  
   208  // TestGetAccountFromStorage_Fails tests that errors received from local storage are handled
   209  // and converted to the appropriate status code
   210  func (s *BackendAccountsSuite) TestGetAccountFromStorage_Fails() {
   211  	ctx := context.Background()
   212  
   213  	scriptExecutor := execmock.NewScriptExecutor(s.T())
   214  
   215  	backend := s.defaultBackend()
   216  	backend.scriptExecMode = IndexQueryModeLocalOnly
   217  	backend.scriptExecutor = scriptExecutor
   218  
   219  	testCases := []struct {
   220  		err        error
   221  		statusCode codes.Code
   222  	}{
   223  		{
   224  			err:        storage.ErrHeightNotIndexed,
   225  			statusCode: codes.OutOfRange,
   226  		},
   227  		{
   228  			err:        storage.ErrNotFound,
   229  			statusCode: codes.NotFound,
   230  		},
   231  		{
   232  			err:        fmt.Errorf("system error"),
   233  			statusCode: codes.Internal,
   234  		},
   235  	}
   236  
   237  	for _, tt := range testCases {
   238  		scriptExecutor.On("GetAccountAtBlockHeight", mock.Anything, s.failingAddress, s.block.Header.Height).
   239  			Return(nil, tt.err).Times(3)
   240  
   241  		s.Run(fmt.Sprintf("GetAccount - fails with %v", tt.err), func() {
   242  			s.testGetAccount(ctx, backend, tt.statusCode)
   243  		})
   244  
   245  		s.Run(fmt.Sprintf("GetAccountAtLatestBlock - fails with %v", tt.err), func() {
   246  			s.testGetAccountAtLatestBlock(ctx, backend, tt.statusCode)
   247  		})
   248  
   249  		s.Run(fmt.Sprintf("GetAccountAtBlockHeight - fails with %v", tt.err), func() {
   250  			s.testGetAccountAtBlockHeight(ctx, backend, tt.statusCode)
   251  		})
   252  	}
   253  }
   254  
   255  // TestGetAccountFromFailover_HappyPath tests that when an error is returned getting an account
   256  // from local storage, the backend will attempt to get the account from an execution node
   257  func (s *BackendAccountsSuite) TestGetAccountFromFailover_HappyPath() {
   258  	ctx := context.Background()
   259  
   260  	s.setupExecutionNodes(s.block)
   261  	s.setupENSuccessResponse(s.block.ID())
   262  
   263  	scriptExecutor := execmock.NewScriptExecutor(s.T())
   264  
   265  	backend := s.defaultBackend()
   266  	backend.scriptExecMode = IndexQueryModeFailover
   267  	backend.scriptExecutor = scriptExecutor
   268  
   269  	for _, errToReturn := range []error{storage.ErrHeightNotIndexed, storage.ErrNotFound} {
   270  		scriptExecutor.On("GetAccountAtBlockHeight", mock.Anything, s.account.Address, s.block.Header.Height).
   271  			Return(nil, errToReturn).Times(3)
   272  
   273  		s.Run(fmt.Sprintf("GetAccount - happy path - recovers %v", errToReturn), func() {
   274  			s.testGetAccount(ctx, backend, codes.OK)
   275  		})
   276  
   277  		s.Run(fmt.Sprintf("GetAccountAtLatestBlock - happy path - recovers %v", errToReturn), func() {
   278  			s.testGetAccountAtLatestBlock(ctx, backend, codes.OK)
   279  		})
   280  
   281  		s.Run(fmt.Sprintf("GetAccountAtBlockHeight - happy path - recovers %v", errToReturn), func() {
   282  			s.testGetAccountAtBlockHeight(ctx, backend, codes.OK)
   283  		})
   284  	}
   285  }
   286  
   287  // TestGetAccountFromFailover_ReturnsENErrors tests that when an error is returned from the execution
   288  // node during a failover, it is returned to the caller.
   289  func (s *BackendAccountsSuite) TestGetAccountFromFailover_ReturnsENErrors() {
   290  	ctx := context.Background()
   291  
   292  	// use a status code that's not used in the API to make sure it's passed through
   293  	statusCode := codes.FailedPrecondition
   294  	errToReturn := status.Error(statusCode, "random error")
   295  
   296  	s.setupExecutionNodes(s.block)
   297  	s.setupENFailingResponse(s.block.ID(), errToReturn)
   298  
   299  	scriptExecutor := execmock.NewScriptExecutor(s.T())
   300  	scriptExecutor.On("GetAccountAtBlockHeight", mock.Anything, s.failingAddress, s.block.Header.Height).
   301  		Return(nil, storage.ErrHeightNotIndexed)
   302  
   303  	backend := s.defaultBackend()
   304  	backend.scriptExecMode = IndexQueryModeFailover
   305  	backend.scriptExecutor = scriptExecutor
   306  
   307  	s.Run("GetAccount - fails with backend err", func() {
   308  		s.testGetAccount(ctx, backend, statusCode)
   309  	})
   310  
   311  	s.Run("GetAccountAtLatestBlock - fails with backend err", func() {
   312  		s.testGetAccountAtLatestBlock(ctx, backend, statusCode)
   313  	})
   314  
   315  	s.Run("GetAccountAtBlockHeight - fails with backend err", func() {
   316  		s.testGetAccountAtBlockHeight(ctx, backend, statusCode)
   317  	})
   318  }
   319  
   320  // TestGetAccountAtLatestBlock_InconsistentState tests that signaler context received error when node state is
   321  // inconsistent
   322  func (s *BackendAccountsSuite) TestGetAccountAtLatestBlockFromStorage_InconsistentState() {
   323  	scriptExecutor := execmock.NewScriptExecutor(s.T())
   324  
   325  	backend := s.defaultBackend()
   326  	backend.scriptExecMode = IndexQueryModeLocalOnly
   327  	backend.scriptExecutor = scriptExecutor
   328  
   329  	s.Run(fmt.Sprintf("GetAccountAtLatestBlock - fails with %v", "inconsistent node's state"), func() {
   330  		s.state.On("Sealed").Return(s.snapshot, nil)
   331  
   332  		err := fmt.Errorf("inconsistent node's state")
   333  		s.snapshot.On("Head").Return(nil, err)
   334  
   335  		signCtxErr := irrecoverable.NewExceptionf("failed to lookup sealed header: %w", err)
   336  		signalerCtx := irrecoverable.WithSignalerContext(context.Background(), irrecoverable.NewMockSignalerContextExpectError(s.T(), context.Background(), signCtxErr))
   337  
   338  		actual, err := backend.GetAccountAtLatestBlock(signalerCtx, s.failingAddress)
   339  		s.Require().Error(err)
   340  		s.Require().Nil(actual)
   341  	})
   342  }
   343  
   344  func (s *BackendAccountsSuite) testGetAccount(ctx context.Context, backend *backendAccounts, statusCode codes.Code) {
   345  	s.state.On("Sealed").Return(s.snapshot, nil).Once()
   346  	s.snapshot.On("Head").Return(s.block.Header, nil).Once()
   347  
   348  	if statusCode == codes.OK {
   349  		actual, err := backend.GetAccount(ctx, s.account.Address)
   350  		s.Require().NoError(err)
   351  		s.Require().Equal(s.account, actual)
   352  	} else {
   353  		actual, err := backend.GetAccount(ctx, s.failingAddress)
   354  		s.Require().Error(err)
   355  		s.Require().Equal(statusCode, status.Code(err))
   356  		s.Require().Nil(actual)
   357  	}
   358  }
   359  
   360  func (s *BackendAccountsSuite) testGetAccountAtLatestBlock(ctx context.Context, backend *backendAccounts, statusCode codes.Code) {
   361  	s.state.On("Sealed").Return(s.snapshot, nil).Once()
   362  	s.snapshot.On("Head").Return(s.block.Header, nil).Once()
   363  
   364  	if statusCode == codes.OK {
   365  		actual, err := backend.GetAccountAtLatestBlock(ctx, s.account.Address)
   366  		s.Require().NoError(err)
   367  		s.Require().Equal(s.account, actual)
   368  	} else {
   369  		actual, err := backend.GetAccountAtLatestBlock(ctx, s.failingAddress)
   370  		s.Require().Error(err)
   371  		s.Require().Equal(statusCode, status.Code(err))
   372  		s.Require().Nil(actual)
   373  	}
   374  }
   375  
   376  func (s *BackendAccountsSuite) testGetAccountAtBlockHeight(ctx context.Context, backend *backendAccounts, statusCode codes.Code) {
   377  	height := s.block.Header.Height
   378  	s.headers.On("BlockIDByHeight", height).Return(s.block.Header.ID(), nil).Once()
   379  
   380  	if statusCode == codes.OK {
   381  		actual, err := backend.GetAccountAtBlockHeight(ctx, s.account.Address, height)
   382  		s.Require().NoError(err)
   383  		s.Require().Equal(s.account, actual)
   384  	} else {
   385  		actual, err := backend.GetAccountAtBlockHeight(ctx, s.failingAddress, height)
   386  		s.Require().Error(err)
   387  		s.Require().Equal(statusCode, status.Code(err))
   388  		s.Require().Nil(actual)
   389  	}
   390  }