Files
node/indexer/index.go
T
Hanzo AI 6de839a515 codec: rip github.com/luxfi/codec from leaf-misc (Wave 1D)
Wave 1D of the luxfi/codec final rip: leaf-misc tier under node/.
This batch removes the direct codec/wrappers, codec/linearcodec, and
codec.Manager imports from p2p network protocol, IP/port encoding,
indexer/keystore on-disk storage, warp socket IPC, x/archivedb +
merkledb internals, and the primary-network wallet UTXO loader.

Migration shape:

(a) codec/wrappers.Packer + length constants
    → node/utils/wrappers (same shape, already in tree as the local
      canonical home for binary IO helpers). 14 files: indexer/,
      network/, utils/ips/, utils/metric/, warp/, x/.

(b) codec.Manager + linearcodec for on-disk container storage
    → hand-rolled big-endian binary marshal/unmarshal. Hard cut;
      no codec-version prefix; payload size bounded explicitly:
      - indexer/codec.go: marshalContainer/unmarshalContainer
      - service/keystore/codec.go: marshalHash/unmarshalHash and
        marshalUser/unmarshalUser, 16 MiB blob cap retained.

(c) codec.Manager for cross-chain UTXO parsing (wallet/network/primary)
    → utxo.ParseUTXO via the ZAP wire dispatcher already registered
      by node/vms/components/lux. Drops the per-chain codec.Manager
      slot from FetchState; AddAllUTXOs no longer takes a codec.
      Underscore-import of vms/components/lux ensures the dispatcher
      is wired even for callers that don't already pull platformvm.

No snow/snowman/snowball/avalanche/avm/subnet residue introduced.
2026-06-05 11:57:14 -07:00

279 lines
8.7 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package indexer
import (
"errors"
"fmt"
"sync"
"github.com/luxfi/database"
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/database/versiondb"
"github.com/luxfi/ids"
"github.com/luxfi/log"
nodeconsensus "github.com/luxfi/node/consensus"
"github.com/luxfi/runtime"
"github.com/luxfi/timer/mockable"
)
// Maximum number of containers IDs that can be fetched at a time in a call to
// GetContainerRange
const MaxFetchedByRange = 1024
var (
// Maps to the byte representation of the next accepted index
nextAcceptedIndexKey = []byte{0x00}
indexToContainerPrefix = []byte{0x01}
containerToIDPrefix = []byte{0x02}
errNoneAccepted = errors.New("no containers have been accepted")
errNumToFetchInvalid = fmt.Errorf("numToFetch must be in [1,%d]", MaxFetchedByRange)
errNoContainerAtIndex = errors.New("no container at index")
_ nodeconsensus.Acceptor = (*index)(nil)
)
// index indexes containers in their order of acceptance
//
// Invariant: index is thread-safe.
// Invariant: index assumes that Accept is called, before the container is
// committed to the database of the VM, in the order they were accepted.
type index struct {
clock *mockable.Clock
lock sync.RWMutex
// The index of the next accepted transaction
nextAcceptedIndex uint64
// When [baseDB] is committed, writes to [baseDB]
vDB *versiondb.Database
baseDB database.Database
// Both [indexToContainer] and [containerToIndex] have [vDB] underneath
// Index --> Container
indexToContainer database.Database
// Container ID --> Index
containerToIndex database.Database
log log.Logger
}
// Create a new thread-safe index.
//
// Invariant: Closes [baseDB] on close.
func newIndex(
baseDB database.Database,
log log.Logger,
clock *mockable.Clock,
) (*index, error) {
vDB := versiondb.New(baseDB)
indexToContainer := prefixdb.New(indexToContainerPrefix, vDB)
containerToIndex := prefixdb.New(containerToIDPrefix, vDB)
i := &index{
clock: clock,
baseDB: baseDB,
vDB: vDB,
indexToContainer: indexToContainer,
containerToIndex: containerToIndex,
log: log,
}
// Get next accepted index from db
nextAcceptedIndex, err := database.GetUInt64(i.vDB, nextAcceptedIndexKey)
if err != nil {
if err == database.ErrNotFound {
nextAcceptedIndex = 0
} else {
return nil, fmt.Errorf("couldn't get next accepted index from database: %w", err)
}
}
i.nextAcceptedIndex = nextAcceptedIndex
i.log.Info("created new index",
"nextAcceptedIndex", i.nextAcceptedIndex,
)
return i, nil
}
// Close this index
func (i *index) Close() error {
return errors.Join(
i.indexToContainer.Close(),
i.containerToIndex.Close(),
i.vDB.Close(),
i.baseDB.Close(),
)
}
// Index that the given transaction is accepted
// Returned error should be treated as fatal; the VM should not commit [containerID]
// or any new containers as accepted.
func (i *index) Accept(rt *runtime.Runtime, containerID ids.ID, containerBytes []byte) error {
i.lock.Lock()
defer i.lock.Unlock()
// It may be the case that in a previous run of this node, this index committed [containerID]
// as accepted and then the node shut down before the VM committed [containerID] as accepted.
// In that case, when the node restarts Accept will be called with the same container.
// Make sure we don't index the same container twice in that event.
_, err := i.containerToIndex.Get(containerID[:])
if err == nil {
i.log.Debug("not indexing already accepted container",
log.Stringer("containerID", containerID),
)
return nil
}
if err != database.ErrNotFound {
return fmt.Errorf("couldn't get whether %s is accepted: %w", containerID, err)
}
i.log.Debug("indexing container",
log.Uint64("nextAcceptedIndex", i.nextAcceptedIndex),
log.Stringer("containerID", containerID),
)
// Persist index --> Container
nextAcceptedIndexBytes := database.PackUInt64(i.nextAcceptedIndex)
bytes, err := marshalContainer(Container{
ID: containerID,
Bytes: containerBytes,
Timestamp: i.clock.Time().UnixNano(),
})
if err != nil {
return fmt.Errorf("couldn't serialize container %s: %w", containerID, err)
}
if err := i.indexToContainer.Put(nextAcceptedIndexBytes, bytes); err != nil {
return fmt.Errorf("couldn't put accepted container %s into index: %w", containerID, err)
}
// Persist container ID --> index
if err := i.containerToIndex.Put(containerID[:], nextAcceptedIndexBytes); err != nil {
return fmt.Errorf("couldn't map container %s to index: %w", containerID, err)
}
// Persist next accepted index
i.nextAcceptedIndex++
if err := database.PutUInt64(i.vDB, nextAcceptedIndexKey, i.nextAcceptedIndex); err != nil {
return fmt.Errorf("couldn't put accepted container %s into index: %w", containerID, err)
}
// Atomically commit [i.vDB], [i.indexToContainer], [i.containerToIndex] to [i.baseDB]
return i.vDB.Commit()
}
// Returns the ID of the [index]th accepted container and the container itself.
// For example, if [index] == 0, returns the first accepted container.
// If [index] == 1, returns the second accepted container, etc.
// Returns an error if there is no container at the given index.
func (i *index) GetContainerByIndex(index uint64) (Container, error) {
i.lock.RLock()
defer i.lock.RUnlock()
return i.getContainerByIndex(index)
}
// Assumes [i.lock] is held
func (i *index) getContainerByIndex(index uint64) (Container, error) {
lastAcceptedIndex, ok := i.lastAcceptedIndex()
if !ok || index > lastAcceptedIndex {
return Container{}, fmt.Errorf("%w %d", errNoContainerAtIndex, index)
}
indexBytes := database.PackUInt64(index)
return i.getContainerByIndexBytes(indexBytes)
}
// [indexBytes] is the byte representation of the index to fetch.
// Assumes [i.lock] is held
func (i *index) getContainerByIndexBytes(indexBytes []byte) (Container, error) {
containerBytes, err := i.indexToContainer.Get(indexBytes)
if err != nil {
i.log.Error("couldn't read container from database",
log.Err(err),
)
return Container{}, fmt.Errorf("couldn't read from database: %w", err)
}
container, err := unmarshalContainer(containerBytes)
if err != nil {
return Container{}, fmt.Errorf("couldn't unmarshal container: %w", err)
}
return container, nil
}
// GetContainerRange returns the IDs of containers at indices
// [startIndex], [startIndex+1], ..., [startIndex+numToFetch-1].
// [startIndex] should be <= i.lastAcceptedIndex().
// [numToFetch] should be in [0, MaxFetchedByRange]
func (i *index) GetContainerRange(startIndex, numToFetch uint64) ([]Container, error) {
// Check arguments for validity
if numToFetch == 0 || numToFetch > MaxFetchedByRange {
return nil, fmt.Errorf("%w but is %d", errNumToFetchInvalid, numToFetch)
}
i.lock.RLock()
defer i.lock.RUnlock()
lastAcceptedIndex, ok := i.lastAcceptedIndex()
if !ok {
return nil, errNoneAccepted
} else if startIndex > lastAcceptedIndex {
return nil, fmt.Errorf("start index (%d) > last accepted index (%d)", startIndex, lastAcceptedIndex)
}
// Calculate the last index we will fetch
lastIndex := min(startIndex+numToFetch-1, lastAcceptedIndex)
// [lastIndex] is always >= [startIndex] so this is safe.
// [numToFetch] is limited to [MaxFetchedByRange] so [containers] is bounded in size.
containers := make([]Container, int(lastIndex)-int(startIndex)+1)
n := 0
var err error
for j := startIndex; j <= lastIndex; j++ {
containers[n], err = i.getContainerByIndex(j)
if err != nil {
return nil, fmt.Errorf("couldn't get container at index %d: %w", j, err)
}
n++
}
return containers, nil
}
// Returns database.ErrNotFound if the container is not indexed as accepted
func (i *index) GetIndex(id ids.ID) (uint64, error) {
i.lock.RLock()
defer i.lock.RUnlock()
return database.GetUInt64(i.containerToIndex, id[:])
}
func (i *index) GetContainerByID(id ids.ID) (Container, error) {
i.lock.RLock()
defer i.lock.RUnlock()
// Read index from database
indexBytes, err := i.containerToIndex.Get(id[:])
if err != nil {
return Container{}, err
}
return i.getContainerByIndexBytes(indexBytes)
}
// GetLastAccepted returns the last accepted container.
// Returns an error if no containers have been accepted.
func (i *index) GetLastAccepted() (Container, error) {
i.lock.RLock()
defer i.lock.RUnlock()
lastAcceptedIndex, exists := i.lastAcceptedIndex()
if !exists {
return Container{}, errNoneAccepted
}
return i.getContainerByIndex(lastAcceptedIndex)
}
// Assumes i.lock is held
// Returns:
//
// 1. The index of the most recently accepted transaction, or 0 if no
// transactions have been accepted
// 2. Whether at least 1 transaction has been accepted
func (i *index) lastAcceptedIndex() (uint64, bool) {
return i.nextAcceptedIndex - 1, i.nextAcceptedIndex != 0
}