mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
X-Chain fully off pcodecs (the last node consumer). kind.go (xkind 1-5), tx.go
(wire.SignedTx envelope), parser (reflect-map + dual LinearCodec DELETED, fx
dispatch by envelope TypeKind/ShapeKind), base/create_asset/operation/import/
export tx, initial_state, operation, block/{standard,parser,builder}, genesis
(native genesis_wire.go: marshalGenesis/parseGenesis + txs.ParseUnsignedTx),
vm/service/static_service/wallet_service/utxo-spender, metrics (pcodecs.Errs ->
errors.Join). ALL xvm test codec threading removed; xvm tests green.
Block-build invariant: writeTxList requires Initialized txs (mempool/parse hand
the builder canonical bytes) — errors on empty tx-bytes rather than a hidden
Initialize side-effect; state_test updated to Initialize its fixtures.
Transport foundation: rpcchainvm NewListener unix-socket fast path gated
LUXD_VM_UNIX_SOCKET=1 (default TCP, non-breaking; api/zap 159b27a infers unix
from socket-path addr). Consumes upstream utxo TransferableOut/In (4ce6a6e).
ZERO real node-side pcodecs consumers remain. vms/pcodecs kept only for 3
external luxfi/chains VMs (Wave B: zkvm/bridgevm/mpcvm) pending their migration.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
557 lines
14 KiB
Go
557 lines
14 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package state
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/luxfi/metric"
|
|
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/database/prefixdb"
|
|
"github.com/luxfi/database/versiondb"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/node/cache"
|
|
"github.com/luxfi/node/cache/lru"
|
|
"github.com/luxfi/node/cache/metercacher"
|
|
"github.com/luxfi/node/vms/xvm/block"
|
|
"github.com/luxfi/node/vms/xvm/txs"
|
|
lux "github.com/luxfi/utxo"
|
|
|
|
// utxo.ParseUTXO factory registration — see
|
|
// vms/components/lux/utxo_parser.go init(). Required for
|
|
// utxoState.GetUTXO to decode wire bytes off disk.
|
|
_ "github.com/luxfi/node/vms/components/lux"
|
|
)
|
|
|
|
const (
|
|
txCacheSize = 8192
|
|
blockIDCacheSize = 8192
|
|
blockCacheSize = 2048
|
|
)
|
|
|
|
var (
|
|
utxoPrefix = []byte("utxo")
|
|
txPrefix = []byte("tx")
|
|
blockIDPrefix = []byte("blockID")
|
|
blockPrefix = []byte("block")
|
|
singletonPrefix = []byte("singleton")
|
|
|
|
// utxoRecordPrefix mirrors the record sub-prefix the luxfi/utxo UTXOState
|
|
// nests inside our utxoDB (utxo_state.go: prefixdb.New([]byte("utxo"), db)).
|
|
// We reconstruct that exact sub-view to enumerate committed UTXO records in
|
|
// ascending-UTXOID order for the execution_root projection. It MUST stay
|
|
// equal to UTXOState's internal record prefix; it is a frozen on-disk
|
|
// layout, not a free parameter.
|
|
utxoRecordPrefix = []byte("utxo")
|
|
|
|
isInitializedKey = []byte{0x00}
|
|
timestampKey = []byte{0x01}
|
|
lastAcceptedKey = []byte{0x02}
|
|
|
|
_ State = (*state)(nil)
|
|
)
|
|
|
|
type ReadOnlyChain interface {
|
|
lux.UTXOGetter
|
|
|
|
// UTXOs returns the occupied UTXOs of this chain in canonical ascending
|
|
// UTXOID order — the deterministic full-set ordering the xvm execution_root
|
|
// projection folds (see vms/xvm/state/xvmroot and the execution_root leaf
|
|
// spec). Enumeration begins immediately after [start] (ids.Empty starts at
|
|
// the first UTXOID) and returns at most [limit] UTXOs; pass a non-positive
|
|
// [limit] for "no bound" (all remaining). The returned slice is in strictly
|
|
// increasing UTXOID order with no duplicates, so a caller can page by
|
|
// passing the last returned UTXOID as the next [start]. A spent/removed UTXO
|
|
// is never returned (it is not part of the occupied set).
|
|
UTXOs(start ids.ID, limit int) ([]*lux.UTXO, error)
|
|
|
|
GetTx(txID ids.ID) (*txs.Tx, error)
|
|
GetBlockIDAtHeight(height uint64) (ids.ID, error)
|
|
GetBlock(blkID ids.ID) (block.Block, error)
|
|
GetLastAccepted() ids.ID
|
|
GetTimestamp() time.Time
|
|
}
|
|
|
|
type Chain interface {
|
|
ReadOnlyChain
|
|
lux.UTXOAdder
|
|
lux.UTXODeleter
|
|
|
|
AddTx(tx *txs.Tx)
|
|
AddBlock(block block.Block)
|
|
SetLastAccepted(blkID ids.ID)
|
|
SetTimestamp(t time.Time)
|
|
}
|
|
|
|
// State persistently maintains a set of UTXOs, transaction, statuses, and
|
|
// singletons.
|
|
type State interface {
|
|
Chain
|
|
lux.UTXOReader
|
|
|
|
IsInitialized() (bool, error)
|
|
SetInitialized() error
|
|
|
|
// InitializeChainState is called after the VM has been linearized. Calling
|
|
// [GetLastAccepted] or [GetTimestamp] before calling this function will
|
|
// return uninitialized data.
|
|
//
|
|
// Invariant: After the chain is linearized, this function is expected to be
|
|
// called during startup.
|
|
InitializeChainState(stopVertexID ids.ID, genesisTimestamp time.Time) error
|
|
|
|
// Discard uncommitted changes to the database.
|
|
Abort()
|
|
|
|
// Commit changes to the base database.
|
|
Commit() error
|
|
|
|
// Returns a batch of unwritten changes that, when written, will commit all
|
|
// pending changes to the base database.
|
|
CommitBatch() (database.Batch, error)
|
|
|
|
// Checksum returns the current state checksum.
|
|
Checksum() ids.ID
|
|
|
|
Close() error
|
|
}
|
|
|
|
/*
|
|
* VMDB
|
|
* |- utxos
|
|
* | '-- utxoDB
|
|
* |-. txs
|
|
* | '-- txID -> tx bytes
|
|
* |-. blockIDs
|
|
* | '-- height -> blockID
|
|
* |-. blocks
|
|
* | '-- blockID -> block bytes
|
|
* '-. singletons
|
|
* |-- initializedKey -> nil
|
|
* |-- timestampKey -> timestamp
|
|
* '-- lastAcceptedKey -> lastAccepted
|
|
*/
|
|
type state struct {
|
|
parser block.Parser
|
|
db *versiondb.Database
|
|
|
|
modifiedUTXOs map[ids.ID]*lux.UTXO // map of modified UTXOID -> *UTXO if the UTXO is nil, it has been removed
|
|
utxoDB database.Database
|
|
utxoState lux.UTXOState
|
|
|
|
// utxoRecordDB is a read-only view of just the committed UTXO records,
|
|
// keyed by raw 32-byte UTXOID. The luxfi/utxo UTXOState (utxoState above)
|
|
// nests its records one prefix deeper inside utxoDB — under utxoPrefix —
|
|
// alongside an address index under indexPrefix. UTXOState exposes
|
|
// point lookups (GetUTXO) and per-address listing (UTXOIDs) but no
|
|
// full-set enumeration, which the canonical execution_root projection
|
|
// requires in ascending-UTXOID order. We mirror UTXOState's record-prefix
|
|
// layout exactly so iterating this handle yields every committed UTXO
|
|
// record (and only records, not index entries) in ascending UTXOID order.
|
|
// It is a frozen on-disk layout, so this mirror reads a stable format
|
|
// rather than inventing one.
|
|
utxoRecordDB database.Database
|
|
|
|
addedTxs map[ids.ID]*txs.Tx // map of txID -> *txs.Tx
|
|
txCache cache.Cacher[ids.ID, *txs.Tx] // cache of txID -> *txs.Tx. If the entry is nil, it is not in the database
|
|
txDB database.Database
|
|
|
|
addedBlockIDs map[uint64]ids.ID // map of height -> blockID
|
|
blockIDCache cache.Cacher[uint64, ids.ID] // cache of height -> blockID. If the entry is ids.Empty, it is not in the database
|
|
blockIDDB database.Database
|
|
|
|
addedBlocks map[ids.ID]block.Block // map of blockID -> Block
|
|
blockCache cache.Cacher[ids.ID, block.Block] // cache of blockID -> Block. If the entry is nil, it is not in the database
|
|
blockDB database.Database
|
|
|
|
// [lastAccepted] is the most recently accepted block.
|
|
lastAccepted, persistedLastAccepted ids.ID
|
|
timestamp, persistedTimestamp time.Time
|
|
singletonDB database.Database
|
|
}
|
|
|
|
func New(
|
|
db *versiondb.Database,
|
|
parser block.Parser,
|
|
metrics metric.Registerer,
|
|
trackChecksums bool,
|
|
) (State, error) {
|
|
utxoDB := prefixdb.New(utxoPrefix, db)
|
|
txDB := prefixdb.New(txPrefix, db)
|
|
blockIDDB := prefixdb.New(blockIDPrefix, db)
|
|
blockDB := prefixdb.New(blockPrefix, db)
|
|
singletonDB := prefixdb.New(singletonPrefix, db)
|
|
|
|
registry, ok := metrics.(metric.Registry)
|
|
if !ok {
|
|
return nil, errors.New("metrics must implement metric.Registry")
|
|
}
|
|
|
|
txCache, err := metercacher.New[ids.ID, *txs.Tx](
|
|
"tx_cache",
|
|
registry,
|
|
lru.NewCache[ids.ID, *txs.Tx](txCacheSize),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
blockIDCache, err := metercacher.New[uint64, ids.ID](
|
|
"block_id_cache",
|
|
registry,
|
|
lru.NewCache[uint64, ids.ID](blockIDCacheSize),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
blockCache, err := metercacher.New[ids.ID, block.Block](
|
|
"block_cache",
|
|
registry,
|
|
lru.NewCache[ids.ID, block.Block](blockCacheSize),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
utxoState, err := lux.NewMeteredUTXOState(utxoDB, registry, trackChecksums)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &state{
|
|
parser: parser,
|
|
db: db,
|
|
|
|
modifiedUTXOs: make(map[ids.ID]*lux.UTXO),
|
|
utxoDB: utxoDB,
|
|
utxoState: utxoState,
|
|
utxoRecordDB: prefixdb.New(utxoRecordPrefix, utxoDB),
|
|
|
|
addedTxs: make(map[ids.ID]*txs.Tx),
|
|
txCache: txCache,
|
|
txDB: txDB,
|
|
|
|
addedBlockIDs: make(map[uint64]ids.ID),
|
|
blockIDCache: blockIDCache,
|
|
blockIDDB: blockIDDB,
|
|
|
|
addedBlocks: make(map[ids.ID]block.Block),
|
|
blockCache: blockCache,
|
|
blockDB: blockDB,
|
|
|
|
singletonDB: singletonDB,
|
|
}, nil
|
|
}
|
|
|
|
func (s *state) GetUTXO(utxoID ids.ID) (*lux.UTXO, error) {
|
|
if utxo, exists := s.modifiedUTXOs[utxoID]; exists {
|
|
if utxo == nil {
|
|
return nil, database.ErrNotFound
|
|
}
|
|
return utxo, nil
|
|
}
|
|
return s.utxoState.GetUTXO(utxoID)
|
|
}
|
|
|
|
func (s *state) UTXOIDs(addr []byte, start ids.ID, limit int) ([]ids.ID, error) {
|
|
return s.utxoState.UTXOIDs(addr, start, limit)
|
|
}
|
|
|
|
func (s *state) AddUTXO(utxo *lux.UTXO) {
|
|
s.modifiedUTXOs[utxo.InputID()] = utxo
|
|
}
|
|
|
|
func (s *state) DeleteUTXO(utxoID ids.ID) {
|
|
s.modifiedUTXOs[utxoID] = nil
|
|
}
|
|
|
|
func (s *state) GetTx(txID ids.ID) (*txs.Tx, error) {
|
|
if tx, exists := s.addedTxs[txID]; exists {
|
|
return tx, nil
|
|
}
|
|
if tx, exists := s.txCache.Get(txID); exists {
|
|
if tx == nil {
|
|
return nil, database.ErrNotFound
|
|
}
|
|
return tx, nil
|
|
}
|
|
|
|
txBytes, err := s.txDB.Get(txID[:])
|
|
if err == database.ErrNotFound {
|
|
s.txCache.Put(txID, nil)
|
|
return nil, database.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// The key was in the database
|
|
tx, err := s.parser.ParseGenesisTx(txBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.txCache.Put(txID, tx)
|
|
return tx, nil
|
|
}
|
|
|
|
func (s *state) AddTx(tx *txs.Tx) {
|
|
txID := tx.ID()
|
|
s.addedTxs[txID] = tx
|
|
}
|
|
|
|
func (s *state) GetBlockIDAtHeight(height uint64) (ids.ID, error) {
|
|
if blkID, exists := s.addedBlockIDs[height]; exists {
|
|
return blkID, nil
|
|
}
|
|
if blkID, cached := s.blockIDCache.Get(height); cached {
|
|
if blkID == ids.Empty {
|
|
return ids.Empty, database.ErrNotFound
|
|
}
|
|
|
|
return blkID, nil
|
|
}
|
|
|
|
heightKey := database.PackUInt64(height)
|
|
|
|
blkIDArrray, err := database.GetID(s.blockIDDB, heightKey)
|
|
if err == database.ErrNotFound {
|
|
s.blockIDCache.Put(height, ids.Empty)
|
|
return ids.Empty, database.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
blkID := ids.ID(blkIDArrray)
|
|
s.blockIDCache.Put(height, blkID)
|
|
return blkID, nil
|
|
}
|
|
|
|
func (s *state) GetBlock(blkID ids.ID) (block.Block, error) {
|
|
if blk, exists := s.addedBlocks[blkID]; exists {
|
|
return blk, nil
|
|
}
|
|
if blk, cached := s.blockCache.Get(blkID); cached {
|
|
if blk == nil {
|
|
return nil, database.ErrNotFound
|
|
}
|
|
|
|
return blk, nil
|
|
}
|
|
|
|
blkBytes, err := s.blockDB.Get(blkID[:])
|
|
if err == database.ErrNotFound {
|
|
s.blockCache.Put(blkID, nil)
|
|
return nil, database.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
blk, err := s.parser.ParseBlock(blkBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.blockCache.Put(blkID, blk)
|
|
return blk, nil
|
|
}
|
|
|
|
func (s *state) AddBlock(block block.Block) {
|
|
blkID := block.ID()
|
|
s.addedBlockIDs[block.Height()] = blkID
|
|
s.addedBlocks[blkID] = block
|
|
}
|
|
|
|
func (s *state) InitializeChainState(stopVertexID ids.ID, genesisTimestamp time.Time) error {
|
|
lastAcceptedArray, err := database.GetID(s.singletonDB, lastAcceptedKey)
|
|
lastAccepted := ids.ID(lastAcceptedArray)
|
|
if err == database.ErrNotFound {
|
|
return s.initializeChainState(stopVertexID, genesisTimestamp)
|
|
} else if err != nil {
|
|
return fmt.Errorf("failed to get last accepted block: %w", err)
|
|
}
|
|
s.lastAccepted = lastAccepted
|
|
s.persistedLastAccepted = lastAccepted
|
|
|
|
timestamp, err := database.GetTimestamp(s.singletonDB, timestampKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get last accepted timestamp: %w", err)
|
|
}
|
|
|
|
s.timestamp = timestamp
|
|
s.persistedTimestamp = timestamp
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *state) initializeChainState(stopVertexID ids.ID, genesisTimestamp time.Time) error {
|
|
genesis, err := block.NewStandardBlock(
|
|
stopVertexID,
|
|
0,
|
|
genesisTimestamp,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize genesis block: %w", err)
|
|
}
|
|
|
|
s.SetLastAccepted(genesis.ID())
|
|
s.SetTimestamp(genesis.Timestamp())
|
|
s.AddBlock(genesis)
|
|
|
|
if err := s.Commit(); err != nil {
|
|
return fmt.Errorf("failed to commit genesis block: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *state) IsInitialized() (bool, error) {
|
|
return s.singletonDB.Has(isInitializedKey)
|
|
}
|
|
|
|
func (s *state) SetInitialized() error {
|
|
return s.singletonDB.Put(isInitializedKey, nil)
|
|
}
|
|
|
|
func (s *state) GetLastAccepted() ids.ID {
|
|
return s.lastAccepted
|
|
}
|
|
|
|
func (s *state) SetLastAccepted(lastAccepted ids.ID) {
|
|
s.lastAccepted = lastAccepted
|
|
}
|
|
|
|
func (s *state) GetTimestamp() time.Time {
|
|
return s.timestamp
|
|
}
|
|
|
|
func (s *state) SetTimestamp(t time.Time) {
|
|
s.timestamp = t
|
|
}
|
|
|
|
func (s *state) Commit() error {
|
|
defer s.Abort()
|
|
batch, err := s.CommitBatch()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return batch.Write()
|
|
}
|
|
|
|
func (s *state) Abort() {
|
|
s.db.Abort()
|
|
}
|
|
|
|
func (s *state) CommitBatch() (database.Batch, error) {
|
|
if err := s.write(); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.db.CommitBatch()
|
|
}
|
|
|
|
func (s *state) Close() error {
|
|
return errors.Join(
|
|
s.utxoDB.Close(),
|
|
s.txDB.Close(),
|
|
s.blockIDDB.Close(),
|
|
s.blockDB.Close(),
|
|
s.singletonDB.Close(),
|
|
s.db.Close(),
|
|
)
|
|
}
|
|
|
|
func (s *state) write() error {
|
|
return errors.Join(
|
|
s.writeUTXOs(),
|
|
s.writeTxs(),
|
|
s.writeBlockIDs(),
|
|
s.writeBlocks(),
|
|
s.writeMetadata(),
|
|
)
|
|
}
|
|
|
|
func (s *state) writeUTXOs() error {
|
|
for utxoID, utxo := range s.modifiedUTXOs {
|
|
delete(s.modifiedUTXOs, utxoID)
|
|
|
|
if utxo != nil {
|
|
if err := s.utxoState.PutUTXO(utxo); err != nil {
|
|
return fmt.Errorf("failed to add utxo: %w", err)
|
|
}
|
|
} else {
|
|
if err := s.utxoState.DeleteUTXO(utxoID); err != nil {
|
|
return fmt.Errorf("failed to remove utxo: %w", err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeTxs() error {
|
|
for txID, tx := range s.addedTxs {
|
|
txBytes := tx.Bytes()
|
|
|
|
delete(s.addedTxs, txID)
|
|
s.txCache.Put(txID, tx)
|
|
if err := s.txDB.Put(txID[:], txBytes); err != nil {
|
|
return fmt.Errorf("failed to add tx: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeBlockIDs() error {
|
|
for height, blkID := range s.addedBlockIDs {
|
|
heightKey := database.PackUInt64(height)
|
|
|
|
delete(s.addedBlockIDs, height)
|
|
s.blockIDCache.Put(height, blkID)
|
|
blkIDArray := ([32]byte)(blkID)
|
|
if err := database.PutID(s.blockIDDB, heightKey, blkIDArray); err != nil {
|
|
return fmt.Errorf("failed to add blockID: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeBlocks() error {
|
|
for blkID, blk := range s.addedBlocks {
|
|
blkBytes := blk.Bytes()
|
|
|
|
delete(s.addedBlocks, blkID)
|
|
s.blockCache.Put(blkID, blk)
|
|
if err := s.blockDB.Put(blkID[:], blkBytes); err != nil {
|
|
return fmt.Errorf("failed to add block: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) writeMetadata() error {
|
|
if !s.persistedTimestamp.Equal(s.timestamp) {
|
|
if err := database.PutTimestamp(s.singletonDB, timestampKey, s.timestamp); err != nil {
|
|
return fmt.Errorf("failed to write timestamp: %w", err)
|
|
}
|
|
s.persistedTimestamp = s.timestamp
|
|
}
|
|
if s.persistedLastAccepted != s.lastAccepted {
|
|
lastAcceptedArray := ([32]byte)(s.lastAccepted)
|
|
if err := database.PutID(s.singletonDB, lastAcceptedKey, lastAcceptedArray); err != nil {
|
|
return fmt.Errorf("failed to write last accepted: %w", err)
|
|
}
|
|
s.persistedLastAccepted = s.lastAccepted
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *state) Checksum() ids.ID {
|
|
return s.utxoState.Checksum()
|
|
}
|