github.com/MetalBlockchain/metalgo@v1.11.9/indexer/indexer_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 indexer 5 6 import ( 7 "errors" 8 "net/http" 9 "sync" 10 "testing" 11 "time" 12 13 "github.com/stretchr/testify/require" 14 "go.uber.org/mock/gomock" 15 16 "github.com/MetalBlockchain/metalgo/api/server" 17 "github.com/MetalBlockchain/metalgo/database/memdb" 18 "github.com/MetalBlockchain/metalgo/database/versiondb" 19 "github.com/MetalBlockchain/metalgo/ids" 20 "github.com/MetalBlockchain/metalgo/snow" 21 "github.com/MetalBlockchain/metalgo/snow/engine/avalanche/vertex" 22 "github.com/MetalBlockchain/metalgo/snow/engine/snowman/block" 23 "github.com/MetalBlockchain/metalgo/snow/snowtest" 24 "github.com/MetalBlockchain/metalgo/utils" 25 "github.com/MetalBlockchain/metalgo/utils/logging" 26 ) 27 28 var ( 29 _ server.PathAdder = (*apiServerMock)(nil) 30 31 errUnimplemented = errors.New("unimplemented") 32 ) 33 34 type apiServerMock struct { 35 timesCalled int 36 bases []string 37 endpoints []string 38 } 39 40 func (a *apiServerMock) AddRoute(_ http.Handler, base, endpoint string) error { 41 a.timesCalled++ 42 a.bases = append(a.bases, base) 43 a.endpoints = append(a.endpoints, endpoint) 44 return nil 45 } 46 47 func (*apiServerMock) AddAliases(string, ...string) error { 48 return errUnimplemented 49 } 50 51 // Test that newIndexer sets fields correctly 52 func TestNewIndexer(t *testing.T) { 53 require := require.New(t) 54 config := Config{ 55 IndexingEnabled: true, 56 AllowIncompleteIndex: true, 57 Log: logging.NoLog{}, 58 DB: memdb.New(), 59 BlockAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 60 TxAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 61 VertexAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 62 APIServer: &apiServerMock{}, 63 ShutdownF: func() {}, 64 } 65 66 idxrIntf, err := NewIndexer(config) 67 require.NoError(err) 68 require.IsType(&indexer{}, idxrIntf) 69 idxr := idxrIntf.(*indexer) 70 require.NotNil(idxr.log) 71 require.NotNil(idxr.db) 72 require.False(idxr.closed) 73 require.NotNil(idxr.pathAdder) 74 require.True(idxr.indexingEnabled) 75 require.True(idxr.allowIncompleteIndex) 76 require.NotNil(idxr.blockIndices) 77 require.Empty(idxr.blockIndices) 78 require.NotNil(idxr.txIndices) 79 require.Empty(idxr.txIndices) 80 require.NotNil(idxr.vtxIndices) 81 require.Empty(idxr.vtxIndices) 82 require.NotNil(idxr.blockAcceptorGroup) 83 require.NotNil(idxr.txAcceptorGroup) 84 require.NotNil(idxr.vertexAcceptorGroup) 85 require.NotNil(idxr.shutdownF) 86 require.False(idxr.hasRunBefore) 87 } 88 89 // Test that [hasRunBefore] is set correctly and that Shutdown is called on close 90 func TestMarkHasRunAndShutdown(t *testing.T) { 91 require := require.New(t) 92 baseDB := memdb.New() 93 db := versiondb.New(baseDB) 94 shutdown := &sync.WaitGroup{} 95 shutdown.Add(1) 96 config := Config{ 97 IndexingEnabled: true, 98 Log: logging.NoLog{}, 99 DB: db, 100 BlockAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 101 TxAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 102 VertexAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 103 APIServer: &apiServerMock{}, 104 ShutdownF: shutdown.Done, 105 } 106 107 idxrIntf, err := NewIndexer(config) 108 require.NoError(err) 109 require.False(idxrIntf.(*indexer).hasRunBefore) 110 require.NoError(db.Commit()) 111 require.NoError(idxrIntf.Close()) 112 shutdown.Wait() 113 shutdown.Add(1) 114 115 config.DB = versiondb.New(baseDB) 116 idxrIntf, err = NewIndexer(config) 117 require.NoError(err) 118 require.IsType(&indexer{}, idxrIntf) 119 idxr := idxrIntf.(*indexer) 120 require.True(idxr.hasRunBefore) 121 require.NoError(idxr.Close()) 122 shutdown.Wait() 123 } 124 125 // Test registering a linear chain and a DAG chain and accepting 126 // some vertices 127 func TestIndexer(t *testing.T) { 128 require := require.New(t) 129 ctrl := gomock.NewController(t) 130 131 baseDB := memdb.New() 132 db := versiondb.New(baseDB) 133 server := &apiServerMock{} 134 config := Config{ 135 IndexingEnabled: true, 136 AllowIncompleteIndex: false, 137 Log: logging.NoLog{}, 138 DB: db, 139 BlockAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 140 TxAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 141 VertexAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 142 APIServer: server, 143 ShutdownF: func() {}, 144 } 145 146 // Create indexer 147 idxrIntf, err := NewIndexer(config) 148 require.NoError(err) 149 require.IsType(&indexer{}, idxrIntf) 150 idxr := idxrIntf.(*indexer) 151 now := time.Now() 152 idxr.clock.Set(now) 153 154 // Assert state is right 155 snow1Ctx := snowtest.Context(t, snowtest.CChainID) 156 chain1Ctx := snowtest.ConsensusContext(snow1Ctx) 157 isIncomplete, err := idxr.isIncomplete(chain1Ctx.ChainID) 158 require.NoError(err) 159 require.False(isIncomplete) 160 previouslyIndexed, err := idxr.previouslyIndexed(chain1Ctx.ChainID) 161 require.NoError(err) 162 require.False(previouslyIndexed) 163 164 // Register this chain, creating a new index 165 chainVM := block.NewMockChainVM(ctrl) 166 idxr.RegisterChain("chain1", chain1Ctx, chainVM) 167 isIncomplete, err = idxr.isIncomplete(chain1Ctx.ChainID) 168 require.NoError(err) 169 require.False(isIncomplete) 170 previouslyIndexed, err = idxr.previouslyIndexed(chain1Ctx.ChainID) 171 require.NoError(err) 172 require.True(previouslyIndexed) 173 require.Equal(1, server.timesCalled) 174 require.Equal("index/chain1", server.bases[0]) 175 require.Equal("/block", server.endpoints[0]) 176 require.Len(idxr.blockIndices, 1) 177 require.Empty(idxr.txIndices) 178 require.Empty(idxr.vtxIndices) 179 180 // Accept a container 181 blkID, blkBytes := ids.GenerateTestID(), utils.RandomBytes(32) 182 expectedContainer := Container{ 183 ID: blkID, 184 Bytes: blkBytes, 185 Timestamp: now.UnixNano(), 186 } 187 188 require.NoError(config.BlockAcceptorGroup.Accept(chain1Ctx, blkID, blkBytes)) 189 190 blkIdx := idxr.blockIndices[chain1Ctx.ChainID] 191 require.NotNil(blkIdx) 192 193 // Verify GetLastAccepted is right 194 gotLastAccepted, err := blkIdx.GetLastAccepted() 195 require.NoError(err) 196 require.Equal(expectedContainer, gotLastAccepted) 197 198 // Verify GetContainerByID is right 199 container, err := blkIdx.GetContainerByID(blkID) 200 require.NoError(err) 201 require.Equal(expectedContainer, container) 202 203 // Verify GetIndex is right 204 index, err := blkIdx.GetIndex(blkID) 205 require.NoError(err) 206 require.Zero(index) 207 208 // Verify GetContainerByIndex is right 209 container, err = blkIdx.GetContainerByIndex(0) 210 require.NoError(err) 211 require.Equal(expectedContainer, container) 212 213 // Verify GetContainerRange is right 214 containers, err := blkIdx.GetContainerRange(0, 1) 215 require.NoError(err) 216 require.Len(containers, 1) 217 require.Equal(expectedContainer, containers[0]) 218 219 // Close the indexer 220 require.NoError(db.Commit()) 221 require.NoError(idxr.Close()) 222 require.True(idxr.closed) 223 // Calling Close again should be fine 224 require.NoError(idxr.Close()) 225 server.timesCalled = 0 226 227 // Re-open the indexer 228 config.DB = versiondb.New(baseDB) 229 idxrIntf, err = NewIndexer(config) 230 require.NoError(err) 231 require.IsType(&indexer{}, idxrIntf) 232 idxr = idxrIntf.(*indexer) 233 now = time.Now() 234 idxr.clock.Set(now) 235 require.Empty(idxr.blockIndices) 236 require.Empty(idxr.txIndices) 237 require.Empty(idxr.vtxIndices) 238 require.True(idxr.hasRunBefore) 239 previouslyIndexed, err = idxr.previouslyIndexed(chain1Ctx.ChainID) 240 require.NoError(err) 241 require.True(previouslyIndexed) 242 hasRun, err := idxr.hasRun() 243 require.NoError(err) 244 require.True(hasRun) 245 isIncomplete, err = idxr.isIncomplete(chain1Ctx.ChainID) 246 require.NoError(err) 247 require.False(isIncomplete) 248 249 // Register the same chain as before 250 idxr.RegisterChain("chain1", chain1Ctx, chainVM) 251 blkIdx = idxr.blockIndices[chain1Ctx.ChainID] 252 require.NotNil(blkIdx) 253 container, err = blkIdx.GetLastAccepted() 254 require.NoError(err) 255 require.Equal(blkID, container.ID) 256 require.Equal(1, server.timesCalled) // block index for chain 257 require.Contains(server.endpoints, "/block") 258 259 // Register a DAG chain 260 snow2Ctx := snowtest.Context(t, snowtest.XChainID) 261 chain2Ctx := snowtest.ConsensusContext(snow2Ctx) 262 isIncomplete, err = idxr.isIncomplete(chain2Ctx.ChainID) 263 require.NoError(err) 264 require.False(isIncomplete) 265 previouslyIndexed, err = idxr.previouslyIndexed(chain2Ctx.ChainID) 266 require.NoError(err) 267 require.False(previouslyIndexed) 268 dagVM := vertex.NewMockLinearizableVM(ctrl) 269 idxr.RegisterChain("chain2", chain2Ctx, dagVM) 270 require.NoError(err) 271 require.Equal(4, server.timesCalled) // block index for chain, block index for dag, vtx index, tx index 272 require.Contains(server.bases, "index/chain2") 273 require.Contains(server.endpoints, "/block") 274 require.Contains(server.endpoints, "/vtx") 275 require.Contains(server.endpoints, "/tx") 276 require.Len(idxr.blockIndices, 2) 277 require.Len(idxr.txIndices, 1) 278 require.Len(idxr.vtxIndices, 1) 279 280 // Accept a vertex 281 vtxID, vtxBytes := ids.GenerateTestID(), utils.RandomBytes(32) 282 expectedVtx := Container{ 283 ID: vtxID, 284 Bytes: vtxBytes, 285 Timestamp: now.UnixNano(), 286 } 287 288 require.NoError(config.VertexAcceptorGroup.Accept(chain2Ctx, vtxID, vtxBytes)) 289 290 vtxIdx := idxr.vtxIndices[chain2Ctx.ChainID] 291 require.NotNil(vtxIdx) 292 293 // Verify GetLastAccepted is right 294 gotLastAccepted, err = vtxIdx.GetLastAccepted() 295 require.NoError(err) 296 require.Equal(expectedVtx, gotLastAccepted) 297 298 // Verify GetContainerByID is right 299 vtx, err := vtxIdx.GetContainerByID(vtxID) 300 require.NoError(err) 301 require.Equal(expectedVtx, vtx) 302 303 // Verify GetIndex is right 304 index, err = vtxIdx.GetIndex(vtxID) 305 require.NoError(err) 306 require.Zero(index) 307 308 // Verify GetContainerByIndex is right 309 vtx, err = vtxIdx.GetContainerByIndex(0) 310 require.NoError(err) 311 require.Equal(expectedVtx, vtx) 312 313 // Verify GetContainerRange is right 314 vtxs, err := vtxIdx.GetContainerRange(0, 1) 315 require.NoError(err) 316 require.Len(vtxs, 1) 317 require.Equal(expectedVtx, vtxs[0]) 318 319 // Accept a tx 320 txID, txBytes := ids.GenerateTestID(), utils.RandomBytes(32) 321 expectedTx := Container{ 322 ID: txID, 323 Bytes: txBytes, 324 Timestamp: now.UnixNano(), 325 } 326 327 require.NoError(config.TxAcceptorGroup.Accept(chain2Ctx, txID, txBytes)) 328 329 txIdx := idxr.txIndices[chain2Ctx.ChainID] 330 require.NotNil(txIdx) 331 332 // Verify GetLastAccepted is right 333 gotLastAccepted, err = txIdx.GetLastAccepted() 334 require.NoError(err) 335 require.Equal(expectedTx, gotLastAccepted) 336 337 // Verify GetContainerByID is right 338 tx, err := txIdx.GetContainerByID(txID) 339 require.NoError(err) 340 require.Equal(expectedTx, tx) 341 342 // Verify GetIndex is right 343 index, err = txIdx.GetIndex(txID) 344 require.NoError(err) 345 require.Zero(index) 346 347 // Verify GetContainerByIndex is right 348 tx, err = txIdx.GetContainerByIndex(0) 349 require.NoError(err) 350 require.Equal(expectedTx, tx) 351 352 // Verify GetContainerRange is right 353 txs, err := txIdx.GetContainerRange(0, 1) 354 require.NoError(err) 355 require.Len(txs, 1) 356 require.Equal(expectedTx, txs[0]) 357 358 // Accepting a vertex shouldn't have caused anything to 359 // happen on the block/tx index. Similar for tx. 360 lastAcceptedTx, err := txIdx.GetLastAccepted() 361 require.NoError(err) 362 require.Equal(txID, lastAcceptedTx.ID) 363 lastAcceptedVtx, err := vtxIdx.GetLastAccepted() 364 require.NoError(err) 365 require.Equal(vtxID, lastAcceptedVtx.ID) 366 lastAcceptedBlk, err := blkIdx.GetLastAccepted() 367 require.NoError(err) 368 require.Equal(blkID, lastAcceptedBlk.ID) 369 370 // Close the indexer again 371 require.NoError(config.DB.(*versiondb.Database).Commit()) 372 require.NoError(idxr.Close()) 373 374 // Re-open one more time and re-register chains 375 config.DB = versiondb.New(baseDB) 376 idxrIntf, err = NewIndexer(config) 377 require.NoError(err) 378 require.IsType(&indexer{}, idxrIntf) 379 idxr = idxrIntf.(*indexer) 380 idxr.RegisterChain("chain1", chain1Ctx, chainVM) 381 idxr.RegisterChain("chain2", chain2Ctx, dagVM) 382 383 // Verify state 384 lastAcceptedTx, err = idxr.txIndices[chain2Ctx.ChainID].GetLastAccepted() 385 require.NoError(err) 386 require.Equal(txID, lastAcceptedTx.ID) 387 lastAcceptedVtx, err = idxr.vtxIndices[chain2Ctx.ChainID].GetLastAccepted() 388 require.NoError(err) 389 require.Equal(vtxID, lastAcceptedVtx.ID) 390 lastAcceptedBlk, err = idxr.blockIndices[chain1Ctx.ChainID].GetLastAccepted() 391 require.NoError(err) 392 require.Equal(blkID, lastAcceptedBlk.ID) 393 } 394 395 // Make sure the indexer doesn't allow incomplete indices unless explicitly allowed 396 func TestIncompleteIndex(t *testing.T) { 397 // Create an indexer with indexing disabled 398 require := require.New(t) 399 ctrl := gomock.NewController(t) 400 401 baseDB := memdb.New() 402 config := Config{ 403 IndexingEnabled: false, 404 AllowIncompleteIndex: false, 405 Log: logging.NoLog{}, 406 DB: versiondb.New(baseDB), 407 BlockAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 408 TxAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 409 VertexAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 410 APIServer: &apiServerMock{}, 411 ShutdownF: func() {}, 412 } 413 idxrIntf, err := NewIndexer(config) 414 require.NoError(err) 415 require.IsType(&indexer{}, idxrIntf) 416 idxr := idxrIntf.(*indexer) 417 require.False(idxr.indexingEnabled) 418 419 // Register a chain 420 snow1Ctx := snowtest.Context(t, snowtest.CChainID) 421 chain1Ctx := snowtest.ConsensusContext(snow1Ctx) 422 isIncomplete, err := idxr.isIncomplete(chain1Ctx.ChainID) 423 require.NoError(err) 424 require.False(isIncomplete) 425 previouslyIndexed, err := idxr.previouslyIndexed(chain1Ctx.ChainID) 426 require.NoError(err) 427 require.False(previouslyIndexed) 428 chainVM := block.NewMockChainVM(ctrl) 429 idxr.RegisterChain("chain1", chain1Ctx, chainVM) 430 isIncomplete, err = idxr.isIncomplete(chain1Ctx.ChainID) 431 require.NoError(err) 432 require.True(isIncomplete) 433 require.Empty(idxr.blockIndices) 434 435 // Close and re-open the indexer, this time with indexing enabled 436 require.NoError(config.DB.(*versiondb.Database).Commit()) 437 require.NoError(idxr.Close()) 438 config.IndexingEnabled = true 439 config.DB = versiondb.New(baseDB) 440 idxrIntf, err = NewIndexer(config) 441 require.NoError(err) 442 require.IsType(&indexer{}, idxrIntf) 443 idxr = idxrIntf.(*indexer) 444 require.True(idxr.indexingEnabled) 445 446 // Register the chain again. Should die due to incomplete index. 447 require.NoError(config.DB.(*versiondb.Database).Commit()) 448 idxr.RegisterChain("chain1", chain1Ctx, chainVM) 449 require.True(idxr.closed) 450 451 // Close and re-open the indexer, this time with indexing enabled 452 // and incomplete index allowed. 453 require.NoError(idxr.Close()) 454 config.AllowIncompleteIndex = true 455 config.DB = versiondb.New(baseDB) 456 idxrIntf, err = NewIndexer(config) 457 require.NoError(err) 458 require.IsType(&indexer{}, idxrIntf) 459 idxr = idxrIntf.(*indexer) 460 require.True(idxr.allowIncompleteIndex) 461 462 // Register the chain again. Should be OK 463 idxr.RegisterChain("chain1", chain1Ctx, chainVM) 464 require.False(idxr.closed) 465 466 // Close the indexer and re-open with indexing disabled and 467 // incomplete index not allowed. 468 require.NoError(idxr.Close()) 469 config.AllowIncompleteIndex = false 470 config.IndexingEnabled = false 471 config.DB = versiondb.New(baseDB) 472 idxrIntf, err = NewIndexer(config) 473 require.NoError(err) 474 require.IsType(&indexer{}, idxrIntf) 475 } 476 477 // Ensure we only index chains in the primary network 478 func TestIgnoreNonDefaultChains(t *testing.T) { 479 require := require.New(t) 480 ctrl := gomock.NewController(t) 481 482 baseDB := memdb.New() 483 db := versiondb.New(baseDB) 484 config := Config{ 485 IndexingEnabled: true, 486 AllowIncompleteIndex: false, 487 Log: logging.NoLog{}, 488 DB: db, 489 BlockAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 490 TxAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 491 VertexAcceptorGroup: snow.NewAcceptorGroup(logging.NoLog{}), 492 APIServer: &apiServerMock{}, 493 ShutdownF: func() {}, 494 } 495 496 // Create indexer 497 idxrIntf, err := NewIndexer(config) 498 require.NoError(err) 499 require.IsType(&indexer{}, idxrIntf) 500 idxr := idxrIntf.(*indexer) 501 502 // Create chain1Ctx for a random subnet + chain. 503 chain1Ctx := snowtest.ConsensusContext(&snow.Context{ 504 ChainID: ids.GenerateTestID(), 505 SubnetID: ids.GenerateTestID(), 506 }) 507 508 // RegisterChain should return without adding an index for this chain 509 chainVM := block.NewMockChainVM(ctrl) 510 idxr.RegisterChain("chain1", chain1Ctx, chainVM) 511 require.Empty(idxr.blockIndices) 512 }