github.com/ava-labs/avalanchego@v1.11.11/vms/rpcchainvm/state_syncable_vm_test.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package rpcchainvm
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"io"
    10  	"testing"
    11  
    12  	"github.com/stretchr/testify/require"
    13  	"go.uber.org/mock/gomock"
    14  
    15  	"github.com/ava-labs/avalanchego/api/metrics"
    16  	"github.com/ava-labs/avalanchego/database/memdb"
    17  	"github.com/ava-labs/avalanchego/database/prefixdb"
    18  	"github.com/ava-labs/avalanchego/ids"
    19  	"github.com/ava-labs/avalanchego/snow"
    20  	"github.com/ava-labs/avalanchego/snow/consensus/snowman/snowmantest"
    21  	"github.com/ava-labs/avalanchego/snow/engine/snowman/block"
    22  	"github.com/ava-labs/avalanchego/snow/engine/snowman/block/blockmock"
    23  	"github.com/ava-labs/avalanchego/snow/engine/snowman/block/blocktest"
    24  	"github.com/ava-labs/avalanchego/snow/snowtest"
    25  	"github.com/ava-labs/avalanchego/utils/logging"
    26  	"github.com/ava-labs/avalanchego/vms/rpcchainvm/grpcutils"
    27  	"github.com/ava-labs/avalanchego/vms/rpcchainvm/runtime"
    28  	"github.com/ava-labs/avalanchego/vms/rpcchainvm/runtime/subprocess"
    29  )
    30  
    31  var (
    32  	_ block.ChainVM         = StateSyncEnabledMock{}
    33  	_ block.StateSyncableVM = StateSyncEnabledMock{}
    34  
    35  	preSummaryHeight = uint64(1789)
    36  	SummaryHeight    = uint64(2022)
    37  
    38  	// a summary to be returned in some UTs
    39  	mockedSummary = &blocktest.StateSummary{
    40  		IDV:     ids.ID{'s', 'u', 'm', 'm', 'a', 'r', 'y', 'I', 'D'},
    41  		HeightV: SummaryHeight,
    42  		BytesV:  []byte("summary"),
    43  	}
    44  
    45  	// last accepted blocks data before and after summary is accepted
    46  	preSummaryBlk = &snowmantest.Block{
    47  		Decidable: snowtest.Decidable{
    48  			IDV:    ids.ID{'f', 'i', 'r', 's', 't', 'B', 'l', 'K'},
    49  			Status: snowtest.Accepted,
    50  		},
    51  		HeightV: preSummaryHeight,
    52  		ParentV: ids.ID{'p', 'a', 'r', 'e', 'n', 't', 'B', 'l', 'k'},
    53  	}
    54  
    55  	summaryBlk = &snowmantest.Block{
    56  		Decidable: snowtest.Decidable{
    57  			IDV:    ids.ID{'s', 'u', 'm', 'm', 'a', 'r', 'y', 'B', 'l', 'K'},
    58  			Status: snowtest.Accepted,
    59  		},
    60  		HeightV: SummaryHeight,
    61  		ParentV: ids.ID{'p', 'a', 'r', 'e', 'n', 't', 'B', 'l', 'k'},
    62  	}
    63  
    64  	// a fictitious error unrelated to state sync
    65  	errBrokenConnectionOrSomething = errors.New("brokenConnectionOrSomething")
    66  	errNothingToParse              = errors.New("nil summary bytes. Nothing to parse")
    67  )
    68  
    69  type StateSyncEnabledMock struct {
    70  	*blockmock.ChainVM
    71  	*blockmock.StateSyncableVM
    72  }
    73  
    74  func stateSyncEnabledTestPlugin(t *testing.T, loadExpectations bool) block.ChainVM {
    75  	// test key is "stateSyncEnabledTestKey"
    76  
    77  	// create mock
    78  	ctrl := gomock.NewController(t)
    79  	ssVM := StateSyncEnabledMock{
    80  		ChainVM:         blockmock.NewChainVM(ctrl),
    81  		StateSyncableVM: blockmock.NewStateSyncableVM(ctrl),
    82  	}
    83  
    84  	if loadExpectations {
    85  		gomock.InOrder(
    86  			ssVM.StateSyncableVM.EXPECT().StateSyncEnabled(gomock.Any()).Return(false, block.ErrStateSyncableVMNotImplemented).Times(1),
    87  			ssVM.StateSyncableVM.EXPECT().StateSyncEnabled(gomock.Any()).Return(false, nil).Times(1),
    88  			ssVM.StateSyncableVM.EXPECT().StateSyncEnabled(gomock.Any()).Return(true, nil).Times(1),
    89  			ssVM.StateSyncableVM.EXPECT().StateSyncEnabled(gomock.Any()).Return(false, errBrokenConnectionOrSomething).Times(1),
    90  		)
    91  	}
    92  
    93  	return ssVM
    94  }
    95  
    96  func getOngoingSyncStateSummaryTestPlugin(t *testing.T, loadExpectations bool) block.ChainVM {
    97  	// test key is "getOngoingSyncStateSummaryTestKey"
    98  
    99  	// create mock
   100  	ctrl := gomock.NewController(t)
   101  	ssVM := StateSyncEnabledMock{
   102  		ChainVM:         blockmock.NewChainVM(ctrl),
   103  		StateSyncableVM: blockmock.NewStateSyncableVM(ctrl),
   104  	}
   105  
   106  	if loadExpectations {
   107  		gomock.InOrder(
   108  			ssVM.StateSyncableVM.EXPECT().GetOngoingSyncStateSummary(gomock.Any()).Return(nil, block.ErrStateSyncableVMNotImplemented).Times(1),
   109  			ssVM.StateSyncableVM.EXPECT().GetOngoingSyncStateSummary(gomock.Any()).Return(mockedSummary, nil).Times(1),
   110  			ssVM.StateSyncableVM.EXPECT().GetOngoingSyncStateSummary(gomock.Any()).Return(nil, errBrokenConnectionOrSomething).Times(1),
   111  		)
   112  	}
   113  
   114  	return ssVM
   115  }
   116  
   117  func getLastStateSummaryTestPlugin(t *testing.T, loadExpectations bool) block.ChainVM {
   118  	// test key is "getLastStateSummaryTestKey"
   119  
   120  	// create mock
   121  	ctrl := gomock.NewController(t)
   122  	ssVM := StateSyncEnabledMock{
   123  		ChainVM:         blockmock.NewChainVM(ctrl),
   124  		StateSyncableVM: blockmock.NewStateSyncableVM(ctrl),
   125  	}
   126  
   127  	if loadExpectations {
   128  		gomock.InOrder(
   129  			ssVM.StateSyncableVM.EXPECT().GetLastStateSummary(gomock.Any()).Return(nil, block.ErrStateSyncableVMNotImplemented).Times(1),
   130  			ssVM.StateSyncableVM.EXPECT().GetLastStateSummary(gomock.Any()).Return(mockedSummary, nil).Times(1),
   131  			ssVM.StateSyncableVM.EXPECT().GetLastStateSummary(gomock.Any()).Return(nil, errBrokenConnectionOrSomething).Times(1),
   132  		)
   133  	}
   134  
   135  	return ssVM
   136  }
   137  
   138  func parseStateSummaryTestPlugin(t *testing.T, loadExpectations bool) block.ChainVM {
   139  	// test key is "parseStateSummaryTestKey"
   140  
   141  	// create mock
   142  	ctrl := gomock.NewController(t)
   143  	ssVM := StateSyncEnabledMock{
   144  		ChainVM:         blockmock.NewChainVM(ctrl),
   145  		StateSyncableVM: blockmock.NewStateSyncableVM(ctrl),
   146  	}
   147  
   148  	if loadExpectations {
   149  		gomock.InOrder(
   150  			ssVM.StateSyncableVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).Return(nil, block.ErrStateSyncableVMNotImplemented).Times(1),
   151  			ssVM.StateSyncableVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).Return(mockedSummary, nil).Times(1),
   152  			ssVM.StateSyncableVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).Return(nil, errNothingToParse).Times(1),
   153  			ssVM.StateSyncableVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).Return(nil, errBrokenConnectionOrSomething).Times(1),
   154  		)
   155  	}
   156  
   157  	return ssVM
   158  }
   159  
   160  func getStateSummaryTestPlugin(t *testing.T, loadExpectations bool) block.ChainVM {
   161  	// test key is "getStateSummaryTestKey"
   162  
   163  	// create mock
   164  	ctrl := gomock.NewController(t)
   165  	ssVM := StateSyncEnabledMock{
   166  		ChainVM:         blockmock.NewChainVM(ctrl),
   167  		StateSyncableVM: blockmock.NewStateSyncableVM(ctrl),
   168  	}
   169  
   170  	if loadExpectations {
   171  		gomock.InOrder(
   172  			ssVM.StateSyncableVM.EXPECT().GetStateSummary(gomock.Any(), gomock.Any()).Return(nil, block.ErrStateSyncableVMNotImplemented).Times(1),
   173  			ssVM.StateSyncableVM.EXPECT().GetStateSummary(gomock.Any(), gomock.Any()).Return(mockedSummary, nil).Times(1),
   174  			ssVM.StateSyncableVM.EXPECT().GetStateSummary(gomock.Any(), gomock.Any()).Return(nil, errBrokenConnectionOrSomething).Times(1),
   175  		)
   176  	}
   177  
   178  	return ssVM
   179  }
   180  
   181  func acceptStateSummaryTestPlugin(t *testing.T, loadExpectations bool) block.ChainVM {
   182  	// test key is "acceptStateSummaryTestKey"
   183  
   184  	// create mock
   185  	ctrl := gomock.NewController(t)
   186  	ssVM := StateSyncEnabledMock{
   187  		ChainVM:         blockmock.NewChainVM(ctrl),
   188  		StateSyncableVM: blockmock.NewStateSyncableVM(ctrl),
   189  	}
   190  
   191  	if loadExpectations {
   192  		gomock.InOrder(
   193  			ssVM.StateSyncableVM.EXPECT().GetStateSummary(gomock.Any(), gomock.Any()).Return(mockedSummary, nil).Times(1),
   194  			ssVM.StateSyncableVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).DoAndReturn(
   195  				func(context.Context, []byte) (block.StateSummary, error) {
   196  					// setup summary to be accepted before returning it
   197  					mockedSummary.AcceptF = func(context.Context) (block.StateSyncMode, error) {
   198  						return block.StateSyncStatic, nil
   199  					}
   200  					return mockedSummary, nil
   201  				},
   202  			).Times(1),
   203  			ssVM.StateSyncableVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).DoAndReturn(
   204  				func(context.Context, []byte) (block.StateSummary, error) {
   205  					// setup summary to be skipped before returning it
   206  					mockedSummary.AcceptF = func(context.Context) (block.StateSyncMode, error) {
   207  						return block.StateSyncSkipped, nil
   208  					}
   209  					return mockedSummary, nil
   210  				},
   211  			).Times(1),
   212  			ssVM.StateSyncableVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).DoAndReturn(
   213  				func(context.Context, []byte) (block.StateSummary, error) {
   214  					// setup summary to fail accept
   215  					mockedSummary.AcceptF = func(context.Context) (block.StateSyncMode, error) {
   216  						return block.StateSyncSkipped, errBrokenConnectionOrSomething
   217  					}
   218  					return mockedSummary, nil
   219  				},
   220  			).Times(1),
   221  		)
   222  	}
   223  
   224  	return ssVM
   225  }
   226  
   227  func lastAcceptedBlockPostStateSummaryAcceptTestPlugin(t *testing.T, loadExpectations bool) block.ChainVM {
   228  	// test key is "lastAcceptedBlockPostStateSummaryAcceptTestKey"
   229  
   230  	// create mock
   231  	ctrl := gomock.NewController(t)
   232  	ssVM := StateSyncEnabledMock{
   233  		ChainVM:         blockmock.NewChainVM(ctrl),
   234  		StateSyncableVM: blockmock.NewStateSyncableVM(ctrl),
   235  	}
   236  
   237  	if loadExpectations {
   238  		gomock.InOrder(
   239  			ssVM.ChainVM.EXPECT().Initialize(
   240  				gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),
   241  				gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),
   242  				gomock.Any(),
   243  			).Return(nil).Times(1),
   244  			ssVM.ChainVM.EXPECT().LastAccepted(gomock.Any()).Return(preSummaryBlk.ID(), nil).Times(1),
   245  			ssVM.ChainVM.EXPECT().GetBlock(gomock.Any(), gomock.Any()).Return(preSummaryBlk, nil).Times(1),
   246  
   247  			ssVM.StateSyncableVM.EXPECT().ParseStateSummary(gomock.Any(), gomock.Any()).DoAndReturn(
   248  				func(context.Context, []byte) (block.StateSummary, error) {
   249  					// setup summary to be accepted before returning it
   250  					mockedSummary.AcceptF = func(context.Context) (block.StateSyncMode, error) {
   251  						return block.StateSyncStatic, nil
   252  					}
   253  					return mockedSummary, nil
   254  				},
   255  			).Times(2),
   256  
   257  			ssVM.ChainVM.EXPECT().SetState(gomock.Any(), gomock.Any()).Return(nil).Times(1),
   258  			ssVM.ChainVM.EXPECT().LastAccepted(gomock.Any()).Return(summaryBlk.ID(), nil).Times(1),
   259  			ssVM.ChainVM.EXPECT().GetBlock(gomock.Any(), gomock.Any()).Return(summaryBlk, nil).Times(1),
   260  		)
   261  	}
   262  
   263  	return ssVM
   264  }
   265  
   266  func buildClientHelper(require *require.Assertions, testKey string) *VMClient {
   267  	process := helperProcess(testKey)
   268  
   269  	log := logging.NewLogger(
   270  		testKey,
   271  		logging.NewWrappedCore(
   272  			logging.Info,
   273  			originalStderr,
   274  			logging.Colors.ConsoleEncoder(),
   275  		),
   276  	)
   277  
   278  	listener, err := grpcutils.NewListener()
   279  	require.NoError(err)
   280  
   281  	status, stopper, err := subprocess.Bootstrap(
   282  		context.Background(),
   283  		listener,
   284  		process,
   285  		&subprocess.Config{
   286  			Stderr:           log,
   287  			Stdout:           io.Discard,
   288  			Log:              log,
   289  			HandshakeTimeout: runtime.DefaultHandshakeTimeout,
   290  		},
   291  	)
   292  	require.NoError(err)
   293  
   294  	clientConn, err := grpcutils.Dial(status.Addr)
   295  	require.NoError(err)
   296  
   297  	return NewClient(clientConn, stopper, status.Pid, nil, metrics.NewPrefixGatherer())
   298  }
   299  
   300  func TestStateSyncEnabled(t *testing.T) {
   301  	require := require.New(t)
   302  	testKey := stateSyncEnabledTestKey
   303  
   304  	// Create and start the plugin
   305  	vm := buildClientHelper(require, testKey)
   306  	defer vm.runtime.Stop(context.Background())
   307  
   308  	// test state sync not implemented
   309  	// Note that enabled == false is returned rather than
   310  	// common.ErrStateSyncableVMNotImplemented
   311  	enabled, err := vm.StateSyncEnabled(context.Background())
   312  	require.NoError(err)
   313  	require.False(enabled)
   314  
   315  	// test state sync disabled
   316  	enabled, err = vm.StateSyncEnabled(context.Background())
   317  	require.NoError(err)
   318  	require.False(enabled)
   319  
   320  	// test state sync enabled
   321  	enabled, err = vm.StateSyncEnabled(context.Background())
   322  	require.NoError(err)
   323  	require.True(enabled)
   324  
   325  	// test a non-special error.
   326  	// TODO: retrieve exact error
   327  	_, err = vm.StateSyncEnabled(context.Background())
   328  	require.Error(err) //nolint:forbidigo // currently returns grpc errors
   329  }
   330  
   331  func TestGetOngoingSyncStateSummary(t *testing.T) {
   332  	require := require.New(t)
   333  	testKey := getOngoingSyncStateSummaryTestKey
   334  
   335  	// Create and start the plugin
   336  	vm := buildClientHelper(require, testKey)
   337  	defer vm.runtime.Stop(context.Background())
   338  
   339  	// test unimplemented case; this is just a guard
   340  	_, err := vm.GetOngoingSyncStateSummary(context.Background())
   341  	require.Equal(block.ErrStateSyncableVMNotImplemented, err)
   342  
   343  	// test successful retrieval
   344  	summary, err := vm.GetOngoingSyncStateSummary(context.Background())
   345  	require.NoError(err)
   346  	require.Equal(mockedSummary.ID(), summary.ID())
   347  	require.Equal(mockedSummary.Height(), summary.Height())
   348  	require.Equal(mockedSummary.Bytes(), summary.Bytes())
   349  
   350  	// test a non-special error.
   351  	// TODO: retrieve exact error
   352  	_, err = vm.GetOngoingSyncStateSummary(context.Background())
   353  	require.Error(err) //nolint:forbidigo // currently returns grpc errors
   354  }
   355  
   356  func TestGetLastStateSummary(t *testing.T) {
   357  	require := require.New(t)
   358  	testKey := getLastStateSummaryTestKey
   359  
   360  	// Create and start the plugin
   361  	vm := buildClientHelper(require, testKey)
   362  	defer vm.runtime.Stop(context.Background())
   363  
   364  	// test unimplemented case; this is just a guard
   365  	_, err := vm.GetLastStateSummary(context.Background())
   366  	require.Equal(block.ErrStateSyncableVMNotImplemented, err)
   367  
   368  	// test successful retrieval
   369  	summary, err := vm.GetLastStateSummary(context.Background())
   370  	require.NoError(err)
   371  	require.Equal(mockedSummary.ID(), summary.ID())
   372  	require.Equal(mockedSummary.Height(), summary.Height())
   373  	require.Equal(mockedSummary.Bytes(), summary.Bytes())
   374  
   375  	// test a non-special error.
   376  	// TODO: retrieve exact error
   377  	_, err = vm.GetLastStateSummary(context.Background())
   378  	require.Error(err) //nolint:forbidigo // currently returns grpc errors
   379  }
   380  
   381  func TestParseStateSummary(t *testing.T) {
   382  	require := require.New(t)
   383  	testKey := parseStateSummaryTestKey
   384  
   385  	// Create and start the plugin
   386  	vm := buildClientHelper(require, testKey)
   387  	defer vm.runtime.Stop(context.Background())
   388  
   389  	// test unimplemented case; this is just a guard
   390  	_, err := vm.ParseStateSummary(context.Background(), mockedSummary.Bytes())
   391  	require.Equal(block.ErrStateSyncableVMNotImplemented, err)
   392  
   393  	// test successful parsing
   394  	summary, err := vm.ParseStateSummary(context.Background(), mockedSummary.Bytes())
   395  	require.NoError(err)
   396  	require.Equal(mockedSummary.ID(), summary.ID())
   397  	require.Equal(mockedSummary.Height(), summary.Height())
   398  	require.Equal(mockedSummary.Bytes(), summary.Bytes())
   399  
   400  	// test parsing nil summary
   401  	_, err = vm.ParseStateSummary(context.Background(), nil)
   402  	require.Error(err) //nolint:forbidigo // currently returns grpc errors
   403  
   404  	// test a non-special error.
   405  	// TODO: retrieve exact error
   406  	_, err = vm.ParseStateSummary(context.Background(), mockedSummary.Bytes())
   407  	require.Error(err) //nolint:forbidigo // currently returns grpc errors
   408  }
   409  
   410  func TestGetStateSummary(t *testing.T) {
   411  	require := require.New(t)
   412  	testKey := getStateSummaryTestKey
   413  
   414  	// Create and start the plugin
   415  	vm := buildClientHelper(require, testKey)
   416  	defer vm.runtime.Stop(context.Background())
   417  
   418  	// test unimplemented case; this is just a guard
   419  	_, err := vm.GetStateSummary(context.Background(), mockedSummary.Height())
   420  	require.Equal(block.ErrStateSyncableVMNotImplemented, err)
   421  
   422  	// test successful retrieval
   423  	summary, err := vm.GetStateSummary(context.Background(), mockedSummary.Height())
   424  	require.NoError(err)
   425  	require.Equal(mockedSummary.ID(), summary.ID())
   426  	require.Equal(mockedSummary.Height(), summary.Height())
   427  	require.Equal(mockedSummary.Bytes(), summary.Bytes())
   428  
   429  	// test a non-special error.
   430  	// TODO: retrieve exact error
   431  	_, err = vm.GetStateSummary(context.Background(), mockedSummary.Height())
   432  	require.Error(err) //nolint:forbidigo // currently returns grpc errors
   433  }
   434  
   435  func TestAcceptStateSummary(t *testing.T) {
   436  	require := require.New(t)
   437  	testKey := acceptStateSummaryTestKey
   438  
   439  	// Create and start the plugin
   440  	vm := buildClientHelper(require, testKey)
   441  	defer vm.runtime.Stop(context.Background())
   442  
   443  	// retrieve the summary first
   444  	summary, err := vm.GetStateSummary(context.Background(), mockedSummary.Height())
   445  	require.NoError(err)
   446  
   447  	// test status Summary
   448  	status, err := summary.Accept(context.Background())
   449  	require.NoError(err)
   450  	require.Equal(block.StateSyncStatic, status)
   451  
   452  	// test skipped Summary
   453  	status, err = summary.Accept(context.Background())
   454  	require.NoError(err)
   455  	require.Equal(block.StateSyncSkipped, status)
   456  
   457  	// test a non-special error.
   458  	// TODO: retrieve exact error
   459  	_, err = summary.Accept(context.Background())
   460  	require.Error(err) //nolint:forbidigo // currently returns grpc errors
   461  }
   462  
   463  // Show that LastAccepted call returns the right answer after a StateSummary
   464  // is accepted AND engine state moves to bootstrapping
   465  func TestLastAcceptedBlockPostStateSummaryAccept(t *testing.T) {
   466  	require := require.New(t)
   467  	testKey := lastAcceptedBlockPostStateSummaryAcceptTestKey
   468  
   469  	// Create and start the plugin
   470  	vm := buildClientHelper(require, testKey)
   471  	defer vm.runtime.Stop(context.Background())
   472  
   473  	// Step 1: initialize VM and check initial LastAcceptedBlock
   474  	ctx := snowtest.Context(t, snowtest.CChainID)
   475  
   476  	require.NoError(vm.Initialize(context.Background(), ctx, prefixdb.New([]byte{}, memdb.New()), nil, nil, nil, nil, nil, nil))
   477  
   478  	blkID, err := vm.LastAccepted(context.Background())
   479  	require.NoError(err)
   480  	require.Equal(preSummaryBlk.ID(), blkID)
   481  
   482  	lastBlk, err := vm.GetBlock(context.Background(), blkID)
   483  	require.NoError(err)
   484  	require.Equal(preSummaryBlk.Height(), lastBlk.Height())
   485  
   486  	// Step 2: pick a state summary to an higher height and accept it
   487  	summary, err := vm.ParseStateSummary(context.Background(), mockedSummary.Bytes())
   488  	require.NoError(err)
   489  
   490  	status, err := summary.Accept(context.Background())
   491  	require.NoError(err)
   492  	require.Equal(block.StateSyncStatic, status)
   493  
   494  	// State Sync accept does not duly update LastAccepted block information
   495  	// since state sync can complete asynchronously
   496  	blkID, err = vm.LastAccepted(context.Background())
   497  	require.NoError(err)
   498  
   499  	lastBlk, err = vm.GetBlock(context.Background(), blkID)
   500  	require.NoError(err)
   501  	require.Equal(preSummaryBlk.Height(), lastBlk.Height())
   502  
   503  	// Setting state to bootstrapping duly update last accepted block
   504  	require.NoError(vm.SetState(context.Background(), snow.Bootstrapping))
   505  
   506  	blkID, err = vm.LastAccepted(context.Background())
   507  	require.NoError(err)
   508  
   509  	lastBlk, err = vm.GetBlock(context.Background(), blkID)
   510  	require.NoError(err)
   511  	require.Equal(summary.Height(), lastBlk.Height())
   512  }