Files
node/indexer/indexer_test.go
zeekay 7cf87b230b node-unified is the node
The module already called itself github.com/luxfi/node while containing no node
in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and
the Go column of the matrix measured a VM plugin. A repo that fetches the thing
it claims to be has not replaced it, so the Go sources move in — 1515 files,
the daemon and everything under it — and the build reaches outside this module
for nothing.

Rust and C++ sit beside it unchanged and are still measured against it by the
differential. That is the point of one repo: the reference implementation and
the two ports are the same checkout, so parity is a thing you can run rather
than a thing you assert.

Verified: builds, and the closure carries no lux-private — checked at build time
rather than believed. The daemon starts twelve chains on a local network
(P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
2026-09-07 16:15:25 -07:00

539 lines
17 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package indexer
import (
"context"
"errors"
"net/http"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/luxfi/database/memdb"
"github.com/luxfi/database/versiondb"
"github.com/luxfi/ids"
"github.com/luxfi/log"
nodeconsensus "github.com/luxfi/node/consensus"
"github.com/luxfi/node/server/http"
"github.com/luxfi/runtime"
"github.com/luxfi/util"
"github.com/luxfi/vm"
)
// testRuntime creates a minimal runtime.Runtime for testing
func testRuntimeWithID(chainID ids.ID) *runtime.Runtime {
return &runtime.Runtime{
NetworkID: 1,
ChainID: chainID,
NodeID: ids.GenerateTestNodeID(),
}
}
// mockChainVM is a simple mock for testing that implements interfaces.VM
type mockChainVM struct{}
func (m *mockChainVM) Initialize(ctx context.Context, init vm.Init) error {
return nil
}
func (m *mockChainVM) Shutdown(ctx context.Context) error {
return nil
}
func (m *mockChainVM) ParseBlock(ctx context.Context, b []byte) (vm.Block, error) {
return nil, nil
}
func (m *mockChainVM) GetBlock(ctx context.Context, id ids.ID) (vm.Block, error) {
return nil, nil
}
func (m *mockChainVM) SetPreference(ctx context.Context, id ids.ID) error {
return nil
}
func (m *mockChainVM) LastAccepted(ctx context.Context) (ids.ID, error) {
return ids.Empty, nil
}
var (
_ server.PathAdder = (*apiServerMock)(nil)
errUnimplemented = errors.New("unimplemented")
)
type apiServerMock struct {
timesCalled int
bases []string
endpoints []string
}
func (a *apiServerMock) AddRoute(_ http.Handler, base, endpoint string) error {
a.timesCalled++
a.bases = append(a.bases, base)
a.endpoints = append(a.endpoints, endpoint)
return nil
}
func (*apiServerMock) AddAliases(string, ...string) error {
return errUnimplemented
}
// Test that newIndexer sets fields correctly
func TestNewIndexer(t *testing.T) {
require := require.New(t)
config := Config{
IndexingEnabled: true,
AllowIncompleteIndex: true,
Log: log.NoLog{},
DB: memdb.New(),
BlockAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
TxAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
VertexAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
APIServer: &apiServerMock{},
ShutdownF: func() {},
}
idxrIntf, err := NewIndexer(config)
require.NoError(err)
require.IsType(&indexer{}, idxrIntf)
idxr := idxrIntf.(*indexer)
require.NotNil(idxr.log)
require.NotNil(idxr.db)
require.False(idxr.closed)
require.NotNil(idxr.pathAdder)
require.True(idxr.indexingEnabled)
require.True(idxr.allowIncompleteIndex)
require.NotNil(idxr.blockIndices)
require.Empty(idxr.blockIndices)
require.NotNil(idxr.txIndices)
require.Empty(idxr.txIndices)
require.NotNil(idxr.vtxIndices)
require.Empty(idxr.vtxIndices)
require.NotNil(idxr.blockAcceptorGroup)
require.NotNil(idxr.txAcceptorGroup)
require.NotNil(idxr.vertexAcceptorGroup)
require.NotNil(idxr.shutdownF)
require.False(idxr.hasRunBefore)
}
// Test that [hasRunBefore] is set correctly and that Shutdown is called on close
func TestMarkHasRunAndShutdown(t *testing.T) {
require := require.New(t)
baseDB := memdb.New()
db := versiondb.New(baseDB)
shutdown := &sync.WaitGroup{}
shutdown.Add(1)
config := Config{
IndexingEnabled: true,
Log: log.NoLog{},
DB: db,
BlockAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
TxAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
VertexAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
APIServer: &apiServerMock{},
ShutdownF: shutdown.Done,
}
idxrIntf, err := NewIndexer(config)
require.NoError(err)
require.False(idxrIntf.(*indexer).hasRunBefore)
require.NoError(db.Commit())
require.NoError(idxrIntf.Close())
shutdown.Wait()
shutdown.Add(1)
config.DB = versiondb.New(baseDB)
idxrIntf, err = NewIndexer(config)
require.NoError(err)
require.IsType(&indexer{}, idxrIntf)
idxr := idxrIntf.(*indexer)
require.True(idxr.hasRunBefore)
require.NoError(idxr.Close())
shutdown.Wait()
}
// Test registering a linear chain and a DAG chain and accepting
// some vertices
func TestIndexer(t *testing.T) {
require := require.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
baseDB := memdb.New()
db := versiondb.New(baseDB)
server := &apiServerMock{}
config := Config{
IndexingEnabled: true,
AllowIncompleteIndex: false,
Log: log.NoLog{},
DB: db,
BlockAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
TxAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
VertexAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
APIServer: server,
ShutdownF: func() {},
}
// Create indexer
idxrIntf, err := NewIndexer(config)
require.NoError(err)
require.IsType(&indexer{}, idxrIntf)
idxr := idxrIntf.(*indexer)
now := time.Now()
idxr.clock.Set(now)
// Assert state is right
// Use a test chain ID
testChainID := ids.GenerateTestID()
chain1RT := testRuntimeWithID(testChainID)
isIncomplete, err := idxr.isIncomplete(testChainID)
require.NoError(err)
require.False(isIncomplete)
previouslyIndexed, err := idxr.previouslyIndexed(testChainID)
require.NoError(err)
require.False(previouslyIndexed)
// Register this chain, creating a new index
chainVM := &mockChainVM{}
idxr.RegisterChain("chain1", chain1RT, chainVM)
t.Logf("After RegisterChain, closed=%v", idxr.closed)
isIncomplete, err = idxr.isIncomplete(testChainID)
require.NoError(err)
require.False(isIncomplete)
previouslyIndexed, err = idxr.previouslyIndexed(testChainID)
require.NoError(err)
require.True(previouslyIndexed)
require.Equal(1, server.timesCalled)
require.Equal("index/chain1", server.bases[0])
// The operations sit beneath what they are about: /v1/index/chain1/block/ops.
require.Equal("/block/ops", server.endpoints[0])
require.Len(idxr.blockIndices, 1)
require.Empty(idxr.txIndices)
require.Empty(idxr.vtxIndices)
// Accept a container
blkID, blkBytes := ids.GenerateTestID(), utils.RandomBytes(32)
expectedContainer := Container{
ID: blkID,
Bytes: blkBytes,
Timestamp: now.UnixNano(),
}
// Accept the block through the index
blkIdx := idxr.blockIndices[testChainID]
require.NotNil(blkIdx)
// Accept the container
require.NoError(blkIdx.Accept(chain1RT, blkID, blkBytes))
// Verify GetLastAccepted is right
gotLastAccepted, err := blkIdx.GetLastAccepted()
require.NoError(err)
require.Equal(expectedContainer, gotLastAccepted)
// Verify GetContainerByID is right
container, err := blkIdx.GetContainerByID(blkID)
require.NoError(err)
require.Equal(expectedContainer, container)
// Verify GetIndex is right
index, err := blkIdx.GetIndex(blkID)
require.NoError(err)
require.Zero(index)
// Verify GetContainerByIndex is right
container, err = blkIdx.GetContainerByIndex(0)
require.NoError(err)
require.Equal(expectedContainer, container)
// Verify GetContainerRange is right
containers, err := blkIdx.GetContainerRange(0, 1)
require.NoError(err)
require.Len(containers, 1)
require.Equal(expectedContainer, containers[0])
// Commit the database before closing the indexer
require.NoError(db.Commit())
// Don't actually close the indexer to avoid closing the database
// Just check that it would close properly
require.False(idxr.closed)
server.timesCalled = 0
// Create a new indexer using the same baseDB to simulate restart
config.DB = versiondb.New(baseDB)
// Create new AcceptorGroups since the old ones still have the chain registered
config.BlockAcceptorGroup = nodeconsensus.NewAcceptorGroup(log.NoLog{})
config.TxAcceptorGroup = nodeconsensus.NewAcceptorGroup(log.NoLog{})
config.VertexAcceptorGroup = nodeconsensus.NewAcceptorGroup(log.NoLog{})
idxrIntf, err = NewIndexer(config)
require.NoError(err)
require.IsType(&indexer{}, idxrIntf)
idxr = idxrIntf.(*indexer)
now = time.Now()
idxr.clock.Set(now)
require.Empty(idxr.blockIndices)
require.Empty(idxr.txIndices)
require.Empty(idxr.vtxIndices)
require.True(idxr.hasRunBefore)
previouslyIndexed, err = idxr.previouslyIndexed(testChainID)
require.NoError(err)
require.True(previouslyIndexed)
hasRun, err := idxr.hasRun()
require.NoError(err)
require.True(hasRun)
isIncomplete, err = idxr.isIncomplete(testChainID)
require.NoError(err)
require.False(isIncomplete)
// Register the same chain as before
chainVM2 := &mockChainVM{}
idxr.RegisterChain("chain1", chain1RT, chainVM2)
blkIdx = idxr.blockIndices[testChainID]
require.NotNil(blkIdx)
container, err = blkIdx.GetLastAccepted()
require.NoError(err)
require.Equal(blkID, container.ID)
require.Equal(1, server.timesCalled) // block index for chain
require.Contains(server.endpoints, "/block/ops")
// Register a second chain (block-based, not DAG)
// Note: DAG/vertex/tx indices are not supported in the new consensus package
chain2ChainID := ids.GenerateTestID()
chain2RT := testRuntimeWithID(chain2ChainID)
isIncomplete, err = idxr.isIncomplete(chain2RT.ChainID)
require.NoError(err)
require.False(isIncomplete)
previouslyIndexed, err = idxr.previouslyIndexed(chain2RT.ChainID)
require.NoError(err)
require.False(previouslyIndexed)
chain2VM := &mockChainVM{}
idxr.RegisterChain("chain2", chain2RT, chain2VM)
require.NoError(err)
// Only block indices are created now (vtx/tx indices not supported)
require.Equal(2, server.timesCalled) // block index for chain1, block index for chain2
require.Contains(server.bases, "index/chain2")
require.Contains(server.endpoints, "/block/ops")
require.Len(idxr.blockIndices, 2)
// No vertex or tx indices in new consensus package
require.Empty(idxr.txIndices)
require.Empty(idxr.vtxIndices)
// Accept a block on chain2
blk2ID, blk2Bytes := ids.GenerateTestID(), utils.RandomBytes(32)
expectedBlk2 := Container{
ID: blk2ID,
Bytes: blk2Bytes,
Timestamp: now.UnixNano(),
}
// Get the block index for chain2
blk2Idx := idxr.blockIndices[chain2ChainID]
require.NotNil(blk2Idx)
// Accept the block
require.NoError(blk2Idx.Accept(chain2RT, blk2ID, blk2Bytes))
// Verify GetLastAccepted is right
gotLastAccepted, err = blk2Idx.GetLastAccepted()
require.NoError(err)
require.Equal(expectedBlk2, gotLastAccepted)
// Verify GetContainerByID is right
blk2, err := blk2Idx.GetContainerByID(blk2ID)
require.NoError(err)
require.Equal(expectedBlk2, blk2)
// Verify GetIndex is right
index, err = blk2Idx.GetIndex(blk2ID)
require.NoError(err)
require.Zero(index)
// Verify GetContainerByIndex is right
blk2, err = blk2Idx.GetContainerByIndex(0)
require.NoError(err)
require.Equal(expectedBlk2, blk2)
// Verify GetContainerRange is right
blks2, err := blk2Idx.GetContainerRange(0, 1)
require.NoError(err)
require.Len(blks2, 1)
require.Equal(expectedBlk2, blks2[0])
// No vertex or tx indices in new consensus package
require.Empty(idxr.vtxIndices)
require.Empty(idxr.txIndices)
// Verify both chains have their expected last accepted blocks
lastAcceptedBlk1, err := blkIdx.GetLastAccepted()
require.NoError(err)
require.Equal(blkID, lastAcceptedBlk1.ID)
lastAcceptedBlk2, err := blk2Idx.GetLastAccepted()
require.NoError(err)
require.Equal(blk2ID, lastAcceptedBlk2.ID)
// Close the indexer again
require.NoError(config.DB.(*versiondb.Database).Commit())
// Don't actually close the indexer to avoid issues with shared database
// Just check that it would close properly
require.False(idxr.closed)
// Re-open one more time and re-register chains
config.DB = versiondb.New(baseDB)
// Create new AcceptorGroups since the old ones were closed
config.BlockAcceptorGroup = nodeconsensus.NewAcceptorGroup(log.NoLog{})
config.TxAcceptorGroup = nodeconsensus.NewAcceptorGroup(log.NoLog{})
config.VertexAcceptorGroup = nodeconsensus.NewAcceptorGroup(log.NoLog{})
idxrIntf, err = NewIndexer(config)
require.NoError(err)
require.IsType(&indexer{}, idxrIntf)
idxr = idxrIntf.(*indexer)
chainVM3 := &mockChainVM{}
idxr.RegisterChain("chain1", chain1RT, chainVM3)
// Re-register chain2 as well
chain2VM2 := &mockChainVM{}
idxr.RegisterChain("chain2", chain2RT, chain2VM2)
// Verify state - both chains should have their blocks
lastAcceptedBlk1Again, err := idxr.blockIndices[testChainID].GetLastAccepted()
require.NoError(err)
require.Equal(blkID, lastAcceptedBlk1Again.ID)
lastAcceptedBlk2Again, err := idxr.blockIndices[chain2ChainID].GetLastAccepted()
require.NoError(err)
require.Equal(blk2ID, lastAcceptedBlk2Again.ID)
// No vertex or tx indices in new consensus package
require.Empty(idxr.vtxIndices)
require.Empty(idxr.txIndices)
}
// Make sure the indexer doesn't allow incomplete indices unless explicitly allowed
func TestIncompleteIndex(t *testing.T) {
// Create an indexer with indexing disabled
require := require.New(t)
baseDB := memdb.New()
config := Config{
IndexingEnabled: false,
AllowIncompleteIndex: false,
Log: log.NoLog{},
DB: versiondb.New(baseDB),
BlockAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
TxAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
VertexAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
APIServer: &apiServerMock{},
ShutdownF: func() {},
}
idxrIntf, err := NewIndexer(config)
require.NoError(err)
require.IsType(&indexer{}, idxrIntf)
idxr := idxrIntf.(*indexer)
require.False(idxr.indexingEnabled)
// Register a chain
testChainID := ids.GenerateTestID()
chain1RT := testRuntimeWithID(testChainID)
isIncomplete, err := idxr.isIncomplete(testChainID)
require.NoError(err)
require.False(isIncomplete)
previouslyIndexed, err := idxr.previouslyIndexed(testChainID)
require.NoError(err)
require.False(previouslyIndexed)
chainVM := &mockChainVM{}
idxr.RegisterChain("chain1", chain1RT, chainVM)
isIncomplete, err = idxr.isIncomplete(testChainID)
require.NoError(err)
require.True(isIncomplete)
require.Empty(idxr.blockIndices)
// Close and re-open the indexer, this time with indexing enabled
require.NoError(config.DB.(*versiondb.Database).Commit())
require.NoError(idxr.Close())
config.IndexingEnabled = true
config.DB = versiondb.New(baseDB)
idxrIntf, err = NewIndexer(config)
require.NoError(err)
require.IsType(&indexer{}, idxrIntf)
idxr = idxrIntf.(*indexer)
require.True(idxr.indexingEnabled)
// Register the chain again. Should die due to incomplete index.
require.NoError(config.DB.(*versiondb.Database).Commit())
chainVM2 := &mockChainVM{}
idxr.RegisterChain("chain1", chain1RT, chainVM2)
require.True(idxr.closed)
// Close and re-open the indexer, this time with indexing enabled
// and incomplete index allowed.
require.NoError(idxr.Close())
config.AllowIncompleteIndex = true
config.DB = versiondb.New(baseDB)
idxrIntf, err = NewIndexer(config)
require.NoError(err)
require.IsType(&indexer{}, idxrIntf)
idxr = idxrIntf.(*indexer)
require.True(idxr.allowIncompleteIndex)
// Register the chain again. Should be OK
chainVM3 := &mockChainVM{}
idxr.RegisterChain("chain1", chain1RT, chainVM3)
require.False(idxr.closed)
// Don't close the indexer to avoid closing the database
// Instead, just mark it as closed for testing purposes
idxr.closed = true
config.AllowIncompleteIndex = false
config.IndexingEnabled = false
// Re-use the same baseDB
config.DB = versiondb.New(baseDB)
idxrIntf, err = NewIndexer(config)
require.NoError(err)
require.IsType(&indexer{}, idxrIntf)
}
// Ensure we only index chains in the primary network
func TestIgnoreNonDefaultChains(t *testing.T) {
require := require.New(t)
baseDB := memdb.New()
db := versiondb.New(baseDB)
config := Config{
IndexingEnabled: true,
AllowIncompleteIndex: false,
Log: log.NoLog{},
DB: db,
BlockAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
TxAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
VertexAcceptorGroup: nodeconsensus.NewAcceptorGroup(log.NoLog{}),
APIServer: &apiServerMock{},
ShutdownF: func() {},
}
// Create indexer
idxrIntf, err := NewIndexer(config)
require.NoError(err)
require.IsType(&indexer{}, idxrIntf)
idxr := idxrIntf.(*indexer)
// Create chain1RT for a chain on a custom network (not mainnet/testnet)
testChainID := ids.GenerateTestID()
chain1RT := testRuntimeWithID(testChainID)
// Set NetworkID to a custom network (not 1=mainnet or 2=testnet)
chain1RT.NetworkID = 99 // Custom network
// RegisterChain should return without adding an index for this chain
chainVM := &mockChainVM{}
idxr.RegisterChain("chain1", chain1RT, chainVM)
require.Empty(idxr.blockIndices)
}