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