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  }