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 }