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 }