mirror of
https://github.com/luxfi/utxo.git
synced 2026-07-27 03:39:23 +00:00
- 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>
298 lines
6.4 KiB
Go
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)
|
|
}
|