Files
zeekay 7cf87b230b node-unified is the node
The module already called itself github.com/luxfi/node while containing no node
in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and
the Go column of the matrix measured a VM plugin. A repo that fetches the thing
it claims to be has not replaced it, so the Go sources move in — 1515 files,
the daemon and everything under it — and the build reaches outside this module
for nothing.

Rust and C++ sit beside it unchanged and are still measured against it by the
differential. That is the point of one repo: the reference implementation and
the two ports are the same checkout, so parity is a thing you can run rather
than a thing you assert.

Verified: builds, and the closure carries no lux-private — checked at build time
rather than believed. The daemon starts twelve chains on a local network
(P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
2026-09-07 16:15:25 -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()
}