Files
node/vms/platformvm/state/state_blocks.go
T
zeekayandHanzo Dev c44cc2289f platformvm builds GREEN off the codec: executor fold + full consumer flip
vms/platformvm/... build + vet PASS with pcodecs/txs.Codec gone from the VM.

Executor semantics (standard_tx_executor):
- registerOwnSet(): shared primitive — per-validator state.L1Validator with
  native owner blobs (txs.MarshalOwner/UnmarshalOwner, no codec), active-set
  capacity check, EndAccumulatedFee=balance+accruedFees, SetNetToL1Conversion
  manager-authority recording (byte-for-byte legacy tail).
- ConvertNetworkTx: promote endomorphism (owner-authorized, folds old
  ConvertNetworkToL1Tx), gated by security.Mode.Manager.
- CreateNetworkTx: base (AddNet/SetNetOwner) + sovereign path registers own set.
- Deleted CreateSovereignL1Tx + ConvertNetworkToL1Tx.
- txs.UnmarshalOwner added: canonical owner marshal/unmarshal pair, no codec.

All ~13 txs.Visitor impls carry ConvertNetworkTx; gossip/block Parse(b) codec-free;
wallet/network/primary off txs.Codec.

KNOWN GAP (pending design decision, NOT silently green): CreateNetworkTx reads
tx.Chains() only to derive managerChainID — it does NOT create the genesis
chains (no AddChain). Atomic-spawn-with-chains vs decomplect-to-CreateChainTx is
the open call. 17 codec-era _test.go parked as .bak for follow-up rewrite.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 14:25:08 -07:00

258 lines
6.7 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package state
import (
"errors"
"fmt"
"math"
"sync"
"time"
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/vms/platformvm/block"
"github.com/luxfi/timer"
)
func (s *state) AddStatelessBlock(block block.Block) {
blkID := block.ID()
s.addedBlockIDs[block.Height()] = blkID
s.addedBlocks[blkID] = block
}
func (s *state) GetStatelessBlock(blockID ids.ID) (block.Block, error) {
if blk, exists := s.addedBlocks[blockID]; exists {
return blk, nil
}
if blk, cached := s.blockCache.Get(blockID); cached {
if blk == nil {
return nil, database.ErrNotFound
}
return blk, nil
}
blkBytes, err := s.blockDB.Get(blockID[:])
if err == database.ErrNotFound {
s.blockCache.Put(blockID, nil)
return nil, database.ErrNotFound
}
if err != nil {
return nil, err
}
blk, _, err := parseStoredBlock(blkBytes)
if err != nil {
return nil, err
}
s.blockCache.Put(blockID, blk)
return blk, nil
}
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
}
blkID, err := database.GetID(s.blockIDDB, database.PackUInt64(height))
if err == database.ErrNotFound {
s.blockIDCache.Put(height, ids.Empty)
return ids.Empty, database.ErrNotFound
}
if err != nil {
return ids.Empty, err
}
s.blockIDCache.Put(height, blkID)
return blkID, nil
}
func (s *state) writeBlocks() error {
for blkID, blk := range s.addedBlocks {
blkBytes := blk.Bytes()
blkHeight := blk.Height()
heightKey := database.PackUInt64(blkHeight)
delete(s.addedBlockIDs, blkHeight)
s.blockIDCache.Put(blkHeight, blkID)
if err := database.PutID(s.blockIDDB, heightKey, blkID); err != nil {
return fmt.Errorf("failed to add blockID: %w", err)
}
delete(s.addedBlocks, blkID)
// Note: Evict is used rather than Put here because blk may end up
// referencing additional data (because of shared byte slices) that
// would not be properly accounted for in the cache sizing.
s.blockCache.Evict(blkID)
if err := s.blockDB.Put(blkID[:], blkBytes); err != nil {
return fmt.Errorf("failed to write block %s: %w", blkID, err)
}
}
return nil
}
// parseStoredBlock returns the block stored under a blockDB value. Blocks are
// stored as their native-ZAP wire bytes (struct-is-wire); block.Parse wraps
// them zero-copy. The second return (legacy-format flag) is always false —
// re-genesis leaves no pre-native block rows on disk.
func parseStoredBlock(blkBytes []byte) (block.Block, bool, error) {
blk, err := block.Parse(blkBytes)
if err != nil {
return nil, false, err
}
return blk, false, nil
}
func (s *state) ReindexBlocks(lock sync.Locker, log log.Logger) error {
has, err := s.singletonDB.Has(BlocksReindexedKey)
if err != nil {
return err
}
if has {
log.Info("blocks already reindexed")
return nil
}
// It is possible that new blocks are added after grabbing this iterator.
// New blocks are guaranteed to be persisted in the new format, so we don't
// need to check them.
blockIterator := s.blockDB.NewIterator()
// Releasing is done using a closure to ensure that updating blockIterator
// will result in having the most recent iterator released when executing
// the deferred function.
defer func() {
blockIterator.Release()
}()
log.Info("starting block reindexing")
var (
startTime = time.Now()
lastCommit = startTime
nextUpdate = startTime.Add(indexLogFrequency)
numIndicesChecked = 0
numIndicesUpdated = 0
)
for blockIterator.Next() {
keyBytes := blockIterator.Key()
valueBytes := blockIterator.Value()
// Skip entries that are not 32 bytes (not block IDs)
if len(keyBytes) != ids.IDLen {
continue
}
blk, isStateBlk, err := parseStoredBlock(valueBytes)
if err != nil {
// Skip entries that can't be parsed as blocks
// This could be metadata or other non-block data
continue
}
blkID := blk.ID()
// This block was previously stored using the legacy format, update the
// index to remove the usage of stateBlk.
if isStateBlk {
blkBytes := blk.Bytes()
if err := s.blockDB.Put(blkID[:], blkBytes); err != nil {
return fmt.Errorf("failed to write block: %w", err)
}
numIndicesUpdated++
}
numIndicesChecked++
now := time.Now()
if now.After(nextUpdate) {
nextUpdate = now.Add(indexLogFrequency)
progress := timer.ProgressFromHash(blkID[:])
eta := timer.EstimateETA(
startTime,
progress,
math.MaxUint64,
)
log.Info("reindexing blocks",
"numIndicesUpdated", numIndicesUpdated,
"numIndicesChecked", numIndicesChecked,
"eta", eta,
)
}
if numIndicesChecked%indexIterationLimit == 0 {
// We must hold the lock during committing to make sure we don't
// attempt to commit to disk while a block is concurrently being
// accepted.
lock.Lock()
err := errors.Join(
s.Commit(),
blockIterator.Error(),
)
lock.Unlock()
if err != nil {
return err
}
// We release the iterator here to allow the underlying database to
// clean up deleted state.
blockIterator.Release()
// We take the minimum here because it's possible that the node is
// currently bootstrapping. This would mean that grabbing the lock
// could take an extremely long period of time; which we should not
// delay processing for.
indexDuration := now.Sub(lastCommit)
sleepDuration := min(
indexIterationSleepMultiplier*indexDuration,
indexIterationSleepCap,
)
time.Sleep(sleepDuration)
// Make sure not to include the sleep duration into the next index
// duration.
lastCommit = time.Now()
blockIterator = s.blockDB.NewIteratorWithStart(blkID[:])
}
}
// Ensure we fully iterated over all blocks before writing that indexing has
// finished.
//
// Note: This is needed because a transient read error could cause the
// iterator to stop early.
if err := blockIterator.Error(); err != nil {
return fmt.Errorf("failed to iterate over historical blocks: %w", err)
}
if err := s.singletonDB.Put(BlocksReindexedKey, nil); err != nil {
return fmt.Errorf("failed to put marked blocks as reindexed: %w", err)
}
// We must hold the lock during committing to make sure we don't attempt to
// commit to disk while a block is concurrently being accepted.
lock.Lock()
defer lock.Unlock()
log.Info("finished block reindexing",
"numIndicesUpdated", numIndicesUpdated,
"numIndicesChecked", numIndicesChecked,
"duration", time.Since(startTime),
)
return s.Commit()
}