Files
node/vms/xvm/state/state.go
T
zeekayandHanzo Dev ddb3fbca93 xvm -> native ZAP struct-is-wire: node-side codec kill COMPLETE
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>
2026-07-12 11:26:36 -07:00

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()
}