Files
zeekayandHanzo Dev 562b447229 Depend on the renamed utility package
github.com/luxfi/utils is now github.com/luxfi/util. Import paths and the
module requirement follow the rename. The local re-export shims under utils/
point at the new path, and tidying moved the old module to an indirect
requirement, which published dependencies still pull in.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-06 04:08:49 -07:00

263 lines
8.2 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package index
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"github.com/luxfi/log"
"github.com/luxfi/metric"
"github.com/luxfi/database"
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/util/wrappers"
lux "github.com/luxfi/utxo"
)
var (
ErrIndexingRequiredFromGenesis = errors.New("running would create incomplete index. Allow incomplete indices or re-sync from genesis with indexing enabled")
ErrCausesIncompleteIndex = errors.New("running would create incomplete index. Allow incomplete indices or enable indexing")
idxKey = []byte("idx")
idxCompleteKey = []byte("complete")
_ AddressTxsIndexer = (*indexer)(nil)
_ AddressTxsIndexer = (*noIndexer)(nil)
)
// AddressTxsIndexer maintains information about which transactions changed
// the balances of which addresses. This includes both transactions that
// increase and decrease an address's balance.
// A transaction is said to change an address's balance if either is true:
// 1) A UTXO that the transaction consumes was at least partially owned by the address.
// 2) A UTXO that the transaction produces is at least partially owned by the address.
type AddressTxsIndexer interface {
// Accept is called when [txID] is accepted.
// Persists data about [txID] and what balances it changed.
// [inputUTXOs] are the UTXOs [txID] consumes.
// [outputUTXOs] are the UTXOs [txID] creates.
// If the error is non-nil, do not persist [txID] to disk as accepted in the VM
Accept(
txID ids.ID,
inputUTXOs []*lux.UTXO,
outputUTXOs []*lux.UTXO,
) error
// Read returns the IDs of transactions that changed [address]'s balance of [assetID].
// The returned transactions are in order of increasing acceptance time.
// The length of the returned slice <= [pageSize].
// [cursor] is the offset to start reading from.
Read(address []byte, assetID ids.ID, cursor, pageSize uint64) ([]ids.ID, error)
}
type indexer struct {
log log.Logger
metrics *indexMetrics
db database.Database
}
// NewIndexer returns a new AddressTxsIndexer.
// The returned indexer ignores UTXOs that are not type secp256k1fx.TransferOutput.
func NewIndexer(
db database.Database,
log log.Logger,
metricsNamespace string,
metricsRegisterer metric.Registerer,
allowIncompleteIndices bool,
) (AddressTxsIndexer, error) {
i := &indexer{
db: db,
log: log,
}
// initialize the indexer
if err := checkIndexStatus(i.db, true, allowIncompleteIndices); err != nil {
return nil, err
}
// initialize the metrics
metrics, err := newMetrics(metricsRegisterer)
if err != nil {
return nil, err
}
i.metrics = metrics
return i, nil
}
// Accept persists which balances [txID] changed.
// Associates all UTXOs in [i.balanceChanges] with transaction [txID].
// The database structure is:
// [address]
// | [assetID]
// | |
// | | "idx" => 2 Running transaction index key, represents the next index
// | | "0" => txID1
// | | "1" => txID1
// See interface documentation AddressTxsIndexer.Accept
func (i *indexer) Accept(txID ids.ID, inputUTXOs []*lux.UTXO, outputUTXOs []*lux.UTXO) error {
utxos := inputUTXOs
// Fetch and add the output UTXOs
utxos = append(utxos, outputUTXOs...)
// convert UTXOs into balance changes
// Address -> AssetID --> exists if the address's balance
// of the asset is changed by processing tx [txID]
// we do this step separately to simplify the write process later
balanceChanges := make(map[string]set.Set[ids.ID])
for _, utxo := range utxos {
out, ok := utxo.Out.(lux.Addressable)
if !ok {
i.log.Debug("skipping UTXO for indexing",
log.Stringer("utxoID", utxo.InputID()),
)
continue
}
for _, addressBytes := range out.Addresses() {
address := string(addressBytes)
addressChanges, exists := balanceChanges[address]
if !exists {
addressChanges = make(set.Set[ids.ID])
balanceChanges[address] = addressChanges
}
addressChanges.Add(utxo.AssetID())
}
}
// Process the balance changes
for address, assetIDs := range balanceChanges {
addressPrefixDB := prefixdb.New([]byte(address), i.db)
for assetID := range assetIDs {
assetPrefixDB := prefixdb.New(assetID[:], addressPrefixDB)
var idx uint64
idxBytes, err := assetPrefixDB.Get(idxKey)
switch err {
case nil:
// index is found, parse stored [idxBytes]
idx = binary.BigEndian.Uint64(idxBytes)
case database.ErrNotFound:
// idx not found; this must be the first entry.
idxBytes = make([]byte, wrappers.LongLen)
default:
// Unexpected error
return fmt.Errorf("unexpected error when indexing txID %s: %w", txID, err)
}
// write the [txID] at the index
i.log.Debug("writing indexed tx to DB",
log.String("address", address),
log.Stringer("assetID", assetID),
log.Uint64("index", idx),
log.Stringer("txID", txID),
)
if err := assetPrefixDB.Put(idxBytes, txID[:]); err != nil {
return fmt.Errorf("failed to write txID while indexing %s: %w", txID, err)
}
// increment and store the index for next use
idx++
binary.BigEndian.PutUint64(idxBytes, idx)
if err := assetPrefixDB.Put(idxKey, idxBytes); err != nil {
return fmt.Errorf("failed to write index txID while indexing %s: %w", txID, err)
}
}
}
i.metrics.numTxsIndexed.Inc()
return nil
}
// Read returns IDs of transactions that changed [address]'s balance of [assetID],
// starting at [cursor], in order of transaction acceptance. e.g. if [cursor] == 1, does
// not return the first transaction that changed the balance. (This is for pagination.)
// Returns at most [pageSize] elements.
// See AddressTxsIndexer
func (i *indexer) Read(address []byte, assetID ids.ID, cursor, pageSize uint64) ([]ids.ID, error) {
// setup prefix DBs
addressTxDB := prefixdb.New(address, i.db)
assetPrefixDB := prefixdb.New(assetID[:], addressTxDB)
// get cursor in bytes
cursorBytes := make([]byte, wrappers.LongLen)
binary.BigEndian.PutUint64(cursorBytes, cursor)
// start reading from the cursor bytes, numeric keys maintain the order (see Accept)
iter := assetPrefixDB.NewIteratorWithStart(cursorBytes)
defer iter.Release()
var txIDs []ids.ID
for uint64(len(txIDs)) < pageSize && iter.Next() {
if bytes.Equal(idxKey, iter.Key()) {
// This key has the next index to use, not a tx ID
continue
}
// get the value and try to convert it to ID
txIDBytes := iter.Value()
txID, err := ids.ToID(txIDBytes)
if err != nil {
return nil, err
}
txIDs = append(txIDs, txID)
}
return txIDs, nil
}
// checkIndexStatus checks the indexing status in the database, returning error if the state
// with respect to provided parameters is invalid
func checkIndexStatus(db database.KeyValueReaderWriter, enableIndexing, allowIncomplete bool) error {
// verify whether the index is complete.
idxComplete, err := database.GetBool(db, idxCompleteKey)
if err == database.ErrNotFound {
// We've not run before. Mark whether indexing is enabled this run.
return database.PutBool(db, idxCompleteKey, enableIndexing)
} else if err != nil {
return err
}
if idxComplete && enableIndexing {
// indexing has been enabled in the past and we're enabling it now
return nil
}
if !idxComplete && enableIndexing && !allowIncomplete {
// In a previous run, we did not index so it's incomplete.
// indexing was disabled before but now we want to index.
return ErrIndexingRequiredFromGenesis
} else if !idxComplete {
// either indexing is disabled, or incomplete indices are ok, so we don't care that index is incomplete
return nil
}
// the index is complete
if !enableIndexing && !allowIncomplete { // indexing is disabled this run
return ErrCausesIncompleteIndex
} else if !enableIndexing {
// running without indexing makes it incomplete
return database.PutBool(db, idxCompleteKey, false)
}
return nil
}
type noIndexer struct{}
func NewNoIndexer(db database.Database, allowIncomplete bool) (AddressTxsIndexer, error) {
return &noIndexer{}, checkIndexStatus(db, false, allowIncomplete)
}
func (*noIndexer) Accept(ids.ID, []*lux.UTXO, []*lux.UTXO) error {
return nil
}
func (*noIndexer) Read([]byte, ids.ID, uint64, uint64) ([]ids.ID, error) {
return nil, nil
}