Files
utxo/utxo_state.go
zeekayandHanzo Dev 6efa1285f9 lint: clean all 21 staticcheck findings — CI green, drop last codec reference
- SA1019: math.Mul64/Add64 -> generic math.Mul/Add (ed25519fx, mldsafx,
  schnorrfx inputs; flow_checker; utxo_fetching); prefixdb.NewNested -> New (alias)
- unused: DELETE dead codecVersion const (last codec reference in utxo — full
  ZAP native now), isSortedAndUniqueOrdered (root + secp256k1fx), mldsafx
  Keychain.get, and all 6 redundant TransferOutput.isState() markers (the
  embedded verify.IsState already provides State membership)
- QF1008: drop embedded-field selectors (utxo_wire, ed25519fx, nftfx,
  propertyfx + tests); ST1005: lowercase Schnorr error string

Behavior-identical; 11/11 packages pass.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-12 22:32:45 -07:00

298 lines
6.4 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package utxo
import (
"errors"
"github.com/luxfi/cache"
"github.com/luxfi/cache/metercacher"
"github.com/luxfi/database"
"github.com/luxfi/database/linkeddb"
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/ids"
"github.com/luxfi/metric"
)
const (
utxoCacheSize = 8192
indexCacheSize = 64
)
var (
utxoPrefix = []byte("utxo")
indexPrefix = []byte("index")
)
// UTXOState is a thin wrapper around a database to provide, caching,
// serialization, and de-serialization for UTXOs.
type UTXOState interface {
UTXOReader
UTXOWriter
// Checksum returns the current UTXOChecksum.
Checksum() ids.ID
}
// UTXOReader is a thin wrapper around a database to provide fetching of UTXOs.
type UTXOReader interface {
UTXOGetter
// UTXOIDs returns the slice of IDs associated with [addr], starting after
// [previous].
// If [previous] is not in the list, starts at beginning.
// Returns at most [limit] IDs.
UTXOIDs(addr []byte, previous ids.ID, limit int) ([]ids.ID, error)
}
// UTXOGetter is a thin wrapper around a database to provide fetching of a UTXO.
type UTXOGetter interface {
// GetUTXO attempts to load a utxo.
GetUTXO(utxoID ids.ID) (*UTXO, error)
}
type UTXOAdder interface {
AddUTXO(utxo *UTXO)
}
type UTXODeleter interface {
DeleteUTXO(utxoID ids.ID)
}
// UTXOWriter is a thin wrapper around a database to provide storage and
// deletion of UTXOs.
type UTXOWriter interface {
// PutUTXO saves the provided utxo to storage.
PutUTXO(utxo *UTXO) error
// DeleteUTXO deletes the provided utxo.
DeleteUTXO(utxoID ids.ID) error
}
type utxoState struct {
// UTXO ID -> *UTXO. If the *UTXO is nil the UTXO doesn't exist
utxoCache cache.Cacher[ids.ID, *UTXO]
utxoDB database.Database
indexDB database.Database
indexCache cache.Cacher[string, linkeddb.LinkedDB]
trackChecksum bool
checksum ids.ID
}
// NewUTXOState returns a UTXOState backed by ZAP-native wire bytes
// (no codec.Manager). Caller MUST have invoked RegisterParseUTXO at
// boot — the fxs dispatch needed to reconstruct *UTXO from wire bytes
// lives in the consumer package to break the import cycle.
func NewUTXOState(
db database.Database,
trackChecksum bool,
) (UTXOState, error) {
s := &utxoState{
utxoCache: &cache.LRU[ids.ID, *UTXO]{Size: utxoCacheSize},
utxoDB: prefixdb.New(utxoPrefix, db),
indexDB: prefixdb.New(indexPrefix, db),
indexCache: &cache.LRU[string, linkeddb.LinkedDB]{Size: indexCacheSize},
trackChecksum: trackChecksum,
}
return s, s.initChecksum()
}
func NewMeteredUTXOState(
db database.Database,
metrics metric.Registerer,
trackChecksum bool,
) (UTXOState, error) {
registry, ok := metrics.(metric.Registry)
if !ok {
return nil, errors.New("metrics must be a Registry")
}
utxoCache, err := metercacher.New[ids.ID, *UTXO](
"utxo_cache",
registry,
&cache.LRU[ids.ID, *UTXO]{Size: utxoCacheSize},
)
if err != nil {
return nil, err
}
indexCache, err := metercacher.New[string, linkeddb.LinkedDB](
"index_cache",
registry,
&cache.LRU[string, linkeddb.LinkedDB]{
Size: indexCacheSize,
},
)
if err != nil {
return nil, err
}
s := &utxoState{
utxoCache: utxoCache,
utxoDB: prefixdb.New(utxoPrefix, db),
indexDB: prefixdb.New(indexPrefix, db),
indexCache: indexCache,
trackChecksum: trackChecksum,
}
return s, s.initChecksum()
}
func (s *utxoState) GetUTXO(utxoID ids.ID) (*UTXO, error) {
if utxo, found := s.utxoCache.Get(utxoID); found {
if utxo == nil {
return nil, database.ErrNotFound
}
return utxo, nil
}
bytes, err := s.utxoDB.Get(utxoID[:])
if err == database.ErrNotFound {
s.utxoCache.Put(utxoID, nil)
return nil, database.ErrNotFound
}
if err != nil {
return nil, err
}
// The key was in the database. Parse via the consumer-registered
// fx-aware ParseUTXO factory (set via RegisterParseUTXO at boot).
utxo, err := ParseUTXO(bytes)
if err != nil {
return nil, err
}
s.utxoCache.Put(utxoID, utxo)
return utxo, nil
}
func (s *utxoState) PutUTXO(utxo *UTXO) error {
// ZAP-native: same bytes flow on the wire and to disk. No
// separate codec.Marshal step.
utxoBytes, err := utxo.WireBytes()
if err != nil {
return err
}
utxoID := utxo.InputID()
s.updateChecksum(utxoID)
s.utxoCache.Put(utxoID, utxo)
if err := s.utxoDB.Put(utxoID[:], utxoBytes); err != nil {
return err
}
addressable, ok := utxo.Out.(Addressable)
if !ok {
return nil
}
addresses := addressable.Addresses()
for _, addr := range addresses {
indexList := s.getIndexDB(addr)
if err := indexList.Put(utxoID[:], nil); err != nil {
return err
}
}
return nil
}
func (s *utxoState) DeleteUTXO(utxoID ids.ID) error {
utxo, err := s.GetUTXO(utxoID)
if err == database.ErrNotFound {
return nil
}
if err != nil {
return err
}
s.updateChecksum(utxoID)
s.utxoCache.Put(utxoID, nil)
if err := s.utxoDB.Delete(utxoID[:]); err != nil {
return err
}
addressable, ok := utxo.Out.(Addressable)
if !ok {
return nil
}
addresses := addressable.Addresses()
for _, addr := range addresses {
indexList := s.getIndexDB(addr)
if err := indexList.Delete(utxoID[:]); err != nil {
return err
}
}
return nil
}
func (s *utxoState) UTXOIDs(addr []byte, start ids.ID, limit int) ([]ids.ID, error) {
indexList := s.getIndexDB(addr)
iter := indexList.NewIteratorWithStart(start[:])
defer iter.Release()
utxoIDs := []ids.ID(nil)
for len(utxoIDs) < limit && iter.Next() {
utxoID, err := ids.ToID(iter.Key())
if err != nil {
return nil, err
}
if utxoID == start {
continue
}
start = ids.Empty
utxoIDs = append(utxoIDs, utxoID)
}
return utxoIDs, iter.Error()
}
func (s *utxoState) Checksum() ids.ID {
return s.checksum
}
func (s *utxoState) getIndexDB(addr []byte) linkeddb.LinkedDB {
addrStr := string(addr)
if indexList, exists := s.indexCache.Get(addrStr); exists {
return indexList
}
indexDB := prefixdb.New(addr, s.indexDB)
indexList := linkeddb.NewDefault(indexDB)
s.indexCache.Put(addrStr, indexList)
return indexList
}
func (s *utxoState) initChecksum() error {
if !s.trackChecksum {
return nil
}
it := s.utxoDB.NewIterator()
defer it.Release()
for it.Next() {
utxoID, err := ids.ToID(it.Key())
if err != nil {
return err
}
s.updateChecksum(utxoID)
}
return it.Error()
}
func (s *utxoState) updateChecksum(modifiedID ids.ID) {
if !s.trackChecksum {
return
}
s.checksum = s.checksum.XOR(modifiedID)
}