Files
node/chains/manager.go
T

3099 lines
109 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package chains
import (
nodeconsensus "github.com/luxfi/node/consensus"
// xvm "github.com/luxfi/node/vms/xvm" // Unused
"context"
"crypto"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/luxfi/database"
"github.com/luxfi/node/server/http"
"github.com/luxfi/node/service/health"
"github.com/luxfi/node/service/metrics"
"github.com/luxfi/vm"
"github.com/luxfi/vm/chains/atomic"
// "github.com/luxfi/database/zapdb" // Unused
dbmanager "github.com/luxfi/database/manager"
"github.com/luxfi/runtime"
// "github.com/luxfi/database/meterdb" // Unused
// "github.com/luxfi/database/prefixdb" // Unused
"github.com/luxfi/ids"
"github.com/luxfi/node/message"
"github.com/luxfi/node/network"
"github.com/luxfi/node/proto/p2p"
// vmpb "github.com/luxfi/node/proto/vm" // Removed - using vm.Ready instead
"github.com/luxfi/warp"
// "github.com/luxfi/consensus/engine/dag/bootstrap/queue" // Unused
// "github.com/luxfi/consensus/engine/dag/state" // Unused
// "github.com/luxfi/consensus/engine/vertex" // Unused
// "github.com/luxfi/consensus/core/tracker"
consensusconfig "github.com/luxfi/consensus/config"
consensuschain "github.com/luxfi/consensus/engine/chain"
consensusdag "github.com/luxfi/consensus/engine/dag"
"github.com/luxfi/vm/chain"
// "github.com/luxfi/vm/chain/syncer"
"github.com/luxfi/consensus/networking/handler"
// "github.com/luxfi/consensus/core/router" // Deprecated - using local ChainRouter interface instead
// "github.com/luxfi/consensus/networking/sender" // Unused after dead code cleanup
"github.com/luxfi/consensus/networking/timeout"
"github.com/luxfi/constants"
"github.com/luxfi/container/buffer"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/filesystem/perms"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
utilmetric "github.com/luxfi/metric"
"github.com/luxfi/node/nets"
"github.com/luxfi/node/staking"
"github.com/luxfi/node/trace"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/node/vms"
validators "github.com/luxfi/validators"
"github.com/luxfi/vm/fx"
// "github.com/luxfi/node/vms/metervm" // Temporarily disabled - needs consensus package updates
"github.com/luxfi/utxo/bls12381fx"
"github.com/luxfi/utxo/ed25519fx"
"github.com/luxfi/utxo/mldsafx"
"github.com/luxfi/utxo/nftfx"
"github.com/luxfi/utxo/propertyfx"
// "github.com/luxfi/node/vms/proposervm"
"github.com/luxfi/utxo/schnorrfx"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/utxo/secp256r1fx"
"github.com/luxfi/utxo/slhdsafx"
// "github.com/luxfi/node/vms/tracedvm" // Temporarily disabled - needs consensus package updates
// "github.com/luxfi/node/proto/p2p" // Available if needed for protobuf parsing
// smcon "github.com/luxfi/vm/chain"
// aveng "github.com/luxfi/consensus/engine/dag"
// avbootstrap "github.com/luxfi/consensus/engine/dag/bootstrap"
// avagetter "github.com/luxfi/consensus/engine/dag/getter"
// smeng "github.com/luxfi/vm/chain"
// smbootstrap "github.com/luxfi/vm/chain/bootstrap"
// consensusgetter "github.com/luxfi/vm/chain/getter"
timetracker "github.com/luxfi/node/network/tracker"
)
const (
ChainLabel = "chain"
defaultChannelSize = 1
initialQueueSize = 3
luxNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "lux"
handlerNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "handler"
meterchainvmNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "meterchainvm"
meterdagvmNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "meterdagvm"
proposervmNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "proposervm"
p2pNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "p2p"
chainNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "linear"
stakeNamespace = constants.PlatformName + utilmetric.NamespaceSeparator + "stake"
)
// ChainRouter is the interface for routing messages to chains.
// This is defined here to avoid circular imports with the node package.
type ChainRouter interface {
AddChain(ctx context.Context, chainID ids.ID, handler handler.Handler)
}
var (
// corely shared VM DB prefix
VMDBPrefix = []byte("vm")
// Bootstrapping prefixes for LinearizableVMs
VertexDBPrefix = []byte("vertex")
VertexBootstrappingDBPrefix = []byte("vertex_bs")
TxBootstrappingDBPrefix = []byte("tx_bs")
BlockBootstrappingDBPrefix = []byte("interval_block_bs")
// Bootstrapping prefixes for ChainVMs
ChainBootstrappingDBPrefix = []byte("interval_bs")
errUnknownVMType = errors.New("the vm should have type lux.DAGVM or chain.ChainVM")
errCreatePlatformVM = errors.New("attempted to create a chain running the PlatformVM")
errNotBootstrapped = errors.New("chains not bootstrapped")
errPartialSyncAsAValidator = errors.New("partial sync should not be configured for a validator")
// fxs lists every Feature eXtension factory the node knows how to load
// when a chain genesis references it. Must stay in sync with the X-Chain
// FxIDs[] block in genesis/builder/builder.go: any fx ID present there
// MUST be registered here, otherwise chain init fails with "fx ... not
// found" at startup. Keep the order and grouping mirroring genesis to
// make drift obvious in review.
fxs = map[ids.ID]fx.Factory{
// Legacy / classical
secp256k1fx.ID: &secp256k1fx.Factory{},
nftfx.ID: &nftfx.Factory{},
propertyfx.ID: &propertyfx.Factory{},
// Post-quantum (FIPS 203/204/205 family)
mldsafx.ID: &mldsafx.Factory{},
slhdsafx.ID: &slhdsafx.Factory{},
// EdDSA / Schnorr / NIST P-256
ed25519fx.ID: &ed25519fx.Factory{},
secp256r1fx.ID: &secp256r1fx.Factory{},
schnorrfx.ID: &schnorrfx.Factory{},
// Pairing-friendly curve
bls12381fx.ID: &bls12381fx.Factory{},
}
_ Manager = (*manager)(nil)
)
// Manager manages the chains running on this node.
// It can:
// - Create a chain
// - Add a registrant. When a chain is created, each registrant calls
// RegisterChain with the new chain as the argument.
// - Manage the aliases of chains
type Manager interface {
ids.Aliaser
// Queues a chain to be created in the future after chain creator is unblocked.
// This is only called from the P-chain thread to create other chains
// Queued chains are created only after P-chain is bootstrapped.
// This assumes only chains in tracked chains are queued.
QueueChainCreation(ChainParameters)
// Add a registrant [r]. Every time a chain is
// created, [r].RegisterChain([new chain]) is called.
AddRegistrant(Registrant)
// Given an alias, return the ID of the chain associated with that alias
Lookup(string) (ids.ID, error)
// Given an alias, return the ID of the VM associated with that alias
LookupVM(string) (ids.ID, error)
// Returns true iff the chain with the given ID exists and is finished bootstrapping
IsBootstrapped(ids.ID) bool
// Starts the chain creator with the initial platform chain parameters, must
// be called once.
StartChainCreator(platformChain ChainParameters) error
// GetChains returns info about all locally running chains.
GetChains() []ChainInfo
// RetryPendingChains re-queues chains that were waiting for the specified VM.
// This is called when a VM is hot-loaded via admin.loadVMs.
RetryPendingChains(vmID ids.ID) int
// GetPendingChains returns the chain parameters waiting for a VM to be loaded.
GetPendingChains(vmID ids.ID) []ChainParameters
Shutdown()
}
// ChainParameters defines the chain being created
type ChainParameters struct {
// The ID of the blockchain being created.
ID ids.ID
// ID of the Net that validates this blockchain.
ChainID ids.ID
// The genesis data of this blockchain's ledger.
GenesisData []byte
// The ID of the vm this blockchain is running.
VMID ids.ID
// The IDs of the feature extensions this blockchain is running.
FxIDs []ids.ID
// Invariant: Only used when [ID] is the P-chain ID.
CustomBeacons validators.Manager
// Name of the chain (used for HTTP routing alias, e.g., /ext/bc/zoo/rpc)
Name string
}
// ChainInfo is the public view of a locally running chain.
// Returned by Manager.GetChains() and exposed via info.GetChains API.
type ChainInfo struct {
ID ids.ID `json:"id"`
Name string `json:"name"`
VMID ids.ID `json:"vmID"`
Bootstrapped bool `json:"bootstrapped"`
}
type chainInfo struct {
Name string
VMID ids.ID
Runtime *runtime.Runtime
VM interface{} // Use interface{} since VM implementations vary
Handler handler.Handler
Engine Engine // Added to handle Start/Stop operations
}
// Engine represents a consensus engine
type Engine interface {
Start(context.Context, bool) error
StopWithError(context.Context, error) error
Context() context.Context
}
// validatorStateWrapper wraps validators.State to implement interfaces.ValidatorState
// noopValidatorState provides a no-op implementation of validators.State for non-staking nodes
type noopValidatorState struct{}
func (n *noopValidatorState) GetValidatorSet(ctx context.Context, height uint64, chainID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
return make(map[ids.NodeID]*validators.GetValidatorOutput), nil
}
func (n *noopValidatorState) GetCurrentValidators(ctx context.Context, height uint64, chainID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
return make(map[ids.NodeID]*validators.GetValidatorOutput), nil
}
func (n *noopValidatorState) GetCurrentHeight(ctx context.Context) (uint64, error) {
return 0, nil
}
func (n *noopValidatorState) GetMinimumHeight(ctx context.Context) (uint64, error) {
return 0, nil
}
func (n *noopValidatorState) GetChainID(netID ids.ID) (ids.ID, error) {
return ids.Empty, nil
}
func (n *noopValidatorState) GetNetworkID(chainID ids.ID) (ids.ID, error) {
return ids.Empty, nil
}
func (n *noopValidatorState) GetWarpValidatorSets(ctx context.Context, heights []uint64, chainIDs []ids.ID) (map[ids.ID]map[uint64]*validators.WarpSet, error) {
result := make(map[ids.ID]map[uint64]*validators.WarpSet)
for _, chainID := range chainIDs {
result[chainID] = make(map[uint64]*validators.WarpSet)
for _, height := range heights {
result[chainID][height] = &validators.WarpSet{
Height: height,
Validators: make(map[ids.NodeID]*validators.WarpValidator),
}
}
}
return result, nil
}
func (n *noopValidatorState) GetWarpValidatorSet(ctx context.Context, height uint64, chainID ids.ID) (*validators.WarpSet, error) {
return &validators.WarpSet{
Height: height,
Validators: make(map[ids.NodeID]*validators.WarpValidator),
}, nil
}
// getValidatorState returns the validator state or a no-op implementation if nil
func getValidatorState(state validators.State) validators.State {
if state != nil {
return state
}
return &noopValidatorState{}
}
// createWarpSigner creates a warp.Signer from a bls.Signer
func createWarpSigner(sk bls.Signer, networkID uint32, chainID ids.ID) warp.Signer {
if sk == nil {
return nil
}
return warp.NewSigner(sk, networkID, chainID)
}
// ChainConfig is configuration settings for the current execution.
// [Config] is the user-provided config blob for the chain.
// [Upgrade] is a chain-specific blob for coordinating upgrades.
type ChainConfig struct {
Config []byte
Upgrade []byte
}
type ManagerConfig struct {
SybilProtectionEnabled bool
StakingTLSSigner crypto.Signer
StakingTLSCert *staking.Certificate
StakingBLSKey bls.Signer
TracingEnabled bool
// Must not be used unless [TracingEnabled] is true as this may be nil.
Tracer trace.Tracer
Log log.Logger
LogFactory log.Factory
VMManager vms.Manager // Manage mappings from vm ID --> vm
BlockAcceptorGroup nodeconsensus.AcceptorGroup
TxAcceptorGroup nodeconsensus.AcceptorGroup
VertexAcceptorGroup nodeconsensus.AcceptorGroup
DB database.Database
MsgCreator message.OutboundMsgBuilder // message creator, shared with network
Router ChainRouter // Routes incoming messages to the appropriate chain
Net network.Network // Sends consensus messages to other validators
Validators validators.Manager // Validators validating on this chain
NodeID ids.NodeID // The ID of this node
NetworkID uint32 // ID of the network this node is connected to
PartialSyncPrimaryNetwork bool
Server server.Server // Handles HTTP API calls
AtomicMemory *atomic.Memory
UTXOAssetID ids.ID
SkipBootstrap bool // Skip bootstrapping and start processing immediately
EnableAutomining bool // Enable automining in POA mode
XChainID ids.ID // ID of the X-Chain,
CChainID ids.ID // ID of the C-Chain,
DChainID ids.ID // ID of the D-Chain (DEX),
CriticalChains set.Set[ids.ID] // Chains that can't exit gracefully
TimeoutManager timeout.Manager // Manages request timeouts when sending messages to other validators
Health health.Registerer
NetConfigs map[ids.ID]nets.Config // ID -> NetConfig
ChainConfigs map[string]ChainConfig // alias -> ChainConfig
// ShutdownNodeFunc allows the chain manager to issue a request to shutdown the node
ShutdownNodeFunc func(exitCode int)
MeterVMEnabled bool // Should each VM be wrapped with a MeterVM
Metrics metrics.MultiGatherer
MeterDBMetrics metrics.MultiGatherer
FrontierPollFrequency time.Duration
ConsensusAppConcurrency int
// Max Time to spend fetching a container and its
// ancestors when responding to a GetAncestors
BootstrapMaxTimeGetAncestors time.Duration
// Max number of containers in an ancestors message sent by this node.
BootstrapAncestorsMaxContainersSent int
// This node will only consider the first [AncestorsMaxContainersReceived]
// containers in an ancestors message it receives.
BootstrapAncestorsMaxContainersReceived int
Upgrades upgrade.Config
// Tracks CPU/disk usage caused by each peer.
ResourceTracker timetracker.ResourceTracker
StateSyncBeacons []ids.NodeID
ChainDataDir string
Nets *Nets
// SecurityProfile is the chain-wide ChainSecurityProfile resolved
// from the genesis pin during node bootstrap (F102). The chain
// manager forwards a ProfileID + ProfileHash pin to every VM
// Initialize call via the VM config bytes so the rpcchainvm plugin
// (coreth C-Chain) inherits the chain-wide posture without
// re-resolving genesis. Closes red-team finding F118.
//
// Nil under classical-compat or pre-locked-profile chains; the
// stamping pass is a no-op in that case.
SecurityProfile *consensusconfig.ChainSecurityProfile
}
type manager struct {
// Note: The string representation of a chain's ID is also considered to be an alias of the chain
// That is, [chainID].String() is an alias for the chain, too
ids.Aliaser
ManagerConfig
// ChainDBManager handles per-chain database instances
chainDBManager *ChainDBManager
// Those notified when a chain is created
registrants []Registrant
// queue that holds chain create requests
chainsQueue buffer.BlockingDeque[ChainParameters]
// unblocks chain creator to start processing the queue
unblockChainCreatorCh chan struct{}
// shutdown the chain creator goroutine if the queue hasn't started to be
// processed.
chainCreatorShutdownCh chan struct{}
chainCreatorExited sync.WaitGroup
// pendingVMChains tracks chains waiting for VMs to be loaded (for hot-loading).
// Key: VM ID that the chain needs
// Value: List of chain parameters waiting for this VM
pendingVMChainsLock sync.RWMutex
pendingVMChains map[ids.ID][]ChainParameters
chainsLock sync.Mutex
// Key: Chain's ID
// Value: The chain
chains map[ids.ID]*chainInfo
// chain++ related interface to allow validators retrieval
validatorState validators.State
luxGatherer metrics.MultiGatherer // chainID
handlerGatherer metrics.MultiGatherer // chainID
meterChainVMGatherer metrics.MultiGatherer // chainID
meterGRAPHVMGatherer metrics.MultiGatherer // chainID
proposervmGatherer metrics.MultiGatherer // chainID
p2pGatherer metrics.MultiGatherer // chainID
linearGatherer metrics.MultiGatherer // chainID
stakeGatherer metrics.MultiGatherer // chainID
vmGatherer map[ids.ID]metrics.MultiGatherer // vmID -> chainID
}
// New returns a new Manager
func New(config *ManagerConfig) (Manager, error) {
luxGatherer := metrics.NewLabelGatherer(ChainLabel)
if err := config.Metrics.Register(luxNamespace, luxGatherer); err != nil {
return nil, err
}
handlerGatherer := metrics.NewLabelGatherer(ChainLabel)
if err := config.Metrics.Register(handlerNamespace, handlerGatherer); err != nil {
return nil, err
}
meterChainVMGatherer := metrics.NewLabelGatherer(ChainLabel)
if err := config.Metrics.Register(meterchainvmNamespace, meterChainVMGatherer); err != nil {
return nil, err
}
meterGRAPHVMGatherer := metrics.NewLabelGatherer(ChainLabel)
if err := config.Metrics.Register(meterdagvmNamespace, meterGRAPHVMGatherer); err != nil {
return nil, err
}
proposervmGatherer := metrics.NewLabelGatherer(ChainLabel)
if err := config.Metrics.Register(proposervmNamespace, proposervmGatherer); err != nil {
return nil, err
}
p2pGatherer := metrics.NewLabelGatherer(ChainLabel)
if err := config.Metrics.Register(p2pNamespace, p2pGatherer); err != nil {
return nil, err
}
linearGatherer := metrics.NewLabelGatherer(ChainLabel)
if err := config.Metrics.Register(chainNamespace, linearGatherer); err != nil {
return nil, err
}
stakeGatherer := metrics.NewLabelGatherer(ChainLabel)
if err := config.Metrics.Register(stakeNamespace, stakeGatherer); err != nil {
return nil, err
}
// Initialize chain database manager using single global ZapDB with prefix isolation
// All chains share one database - G-Chain (dgraph) can index the entire database for GraphQL queries
chainDBManager := NewChainDBManager(ChainDBManagerConfig{
DB: config.DB,
Log: config.Log,
})
return &manager{
Aliaser: ids.NewAliaser(),
ManagerConfig: *config,
chainDBManager: chainDBManager,
chains: make(map[ids.ID]*chainInfo),
chainsQueue: buffer.NewUnboundedBlockingDeque[ChainParameters](initialQueueSize),
unblockChainCreatorCh: make(chan struct{}),
chainCreatorShutdownCh: make(chan struct{}),
pendingVMChains: make(map[ids.ID][]ChainParameters),
luxGatherer: luxGatherer,
handlerGatherer: handlerGatherer,
meterChainVMGatherer: meterChainVMGatherer,
meterGRAPHVMGatherer: meterGRAPHVMGatherer,
proposervmGatherer: proposervmGatherer,
p2pGatherer: p2pGatherer,
linearGatherer: linearGatherer,
stakeGatherer: stakeGatherer,
vmGatherer: make(map[ids.ID]metrics.MultiGatherer),
}, nil
}
// QueueChainCreation queues a chain creation request
// Invariant: Tracked Net must be checked before calling this function
func (m *manager) QueueChainCreation(chainParams ChainParameters) {
m.Log.Info("QueueChainCreation called",
log.String("vmID", chainParams.VMID.String()),
log.String("EVMID", constants.EVMID.String()),
log.Bool("vmIDEqualsEVMID", chainParams.VMID == constants.EVMID),
)
// Register blockchain→chain mapping with the network layer so gossip
// can resolve which validator set to use for this blockchain's blocks.
if chainParams.ChainID != constants.PrimaryNetworkID && m.Net != nil {
m.Net.RegisterBlockchainNetwork(chainParams.ID, chainParams.ChainID)
}
if sb, _ := m.Nets.GetOrCreate(chainParams.ChainID); !sb.AddChain(chainParams.ID) {
m.Log.Debug("skipping chain creation",
log.String("reason", "chain already staged"),
log.Stringer("chainID", chainParams.ChainID),
log.Stringer("chainID", chainParams.ID),
log.Stringer("vmID", chainParams.VMID),
)
return
}
if ok := m.chainsQueue.PushRight(chainParams); !ok {
m.Log.Warn("skipping chain creation",
log.String("reason", "couldn't enqueue chain"),
log.Stringer("chainID", chainParams.ChainID),
log.Stringer("chainID", chainParams.ID),
log.Stringer("vmID", chainParams.VMID),
)
}
}
// createChain creates and starts the chain
//
// Note: it is expected for the net to already have the chain registered as
// bootstrapping before this function is called
func (m *manager) createChain(chainParams ChainParameters) {
m.Log.Info("creating chain",
log.Stringer("chainID", chainParams.ChainID),
log.Stringer("chainID", chainParams.ID),
log.Stringer("vmID", chainParams.VMID),
)
sb, _ := m.Nets.GetOrCreate(chainParams.ChainID)
// Note: buildChain builds all chain's relevant objects (notably engine and handler)
// but does not start their operations. Starting of the handler (which could potentially
// issue some internal messages), is delayed until chain dispatching is started and
// the chain is registered in the manager. This ensures that no message generated by handler
// upon start is dropped.
chain, err := m.buildChain(chainParams, sb)
if chain == nil && err == nil {
m.Log.Info("chain skipped", log.Stringer("chainID", chainParams.ID))
return
}
if err != nil {
// Special handling for X-Chain in single validator mode
// Allow the node to continue without X-Chain when it fails with VM type error
// X-Chain ID: w68fJWq2nmQYuEKvbKRrKvDXB8xGnzuVGpoosXF3YV2N3G6nY
xChainID, _ := ids.FromString("w68fJWq2nmQYuEKvbKRrKvDXB8xGnzuVGpoosXF3YV2N3G6nY")
isXChain := chainParams.ID == xChainID
isVMTypeError := err == errUnknownVMType
skipBootstrapMode := m.SkipBootstrap
// If X-Chain fails with VM type error in single validator mode, just log and continue
if isXChain && isVMTypeError && skipBootstrapMode {
chainAlias := m.PrimaryAliasOrDefault(chainParams.ID)
m.Log.Warn("X-Chain creation failed in single validator mode - continuing without X-Chain",
log.Stringer("chainID", chainParams.ChainID),
log.Stringer("chainID", chainParams.ID),
log.String("chainAlias", chainAlias),
log.Stringer("vmID", chainParams.VMID),
log.String("errorString", fmt.Sprintf("%v", err)),
log.Err(err),
)
// Register a health check that indicates X-Chain is not running
healthCheckErr := fmt.Errorf("X-Chain not running in single validator mode: %w", err)
err := m.Health.RegisterHealthCheck(
chainAlias,
health.CheckerFunc(func(context.Context) (interface{}, error) {
return nil, healthCheckErr
}),
chainParams.ChainID.String(),
)
if err != nil {
m.Log.Error("failed to register X-Chain health check",
log.Stringer("chainID", chainParams.ID),
log.String("chainAlias", chainAlias),
log.Err(err),
)
}
return
}
if m.CriticalChains.Contains(chainParams.ID) {
// Shut down if we fail to create a required chain (i.e. X, P or C)
// unless it's X-Chain with VM type error in single validator mode (handled above)
m.Log.Error("error creating required chain",
log.Stringer("chainID", chainParams.ChainID),
log.Stringer("chainID", chainParams.ID),
log.Stringer("vmID", chainParams.VMID),
log.String("errorString", fmt.Sprintf("%v", err)),
log.String("errorType", fmt.Sprintf("%T", err)),
log.Err(err),
)
go m.ShutdownNodeFunc(1)
return
}
chainAlias := m.PrimaryAliasOrDefault(chainParams.ID)
// Plugin-not-loaded is a deliberate skip, not a failure.
//
// Each validator opts into chains by loading their VM plugin
// (Liquid loads /dex/fhe; doesn't load Lux's upstream
// EVM plugin for C-Chain because Liquid runs its own EVM as a
// chain on the primary's P-chain). When the chain manager hits a
// chain whose VM isn't registered, that's the validator's "no I
// don't validate this one" signal — log info, mark the slot
// bootstrapped, and return WITHOUT registering a failing health
// check. The chain stays in pending state for hot-load if the
// plugin shows up later (e.g. via lpm install).
//
// The old behavior (always register a failing health check) made
// /ext/health return 503 for any opted-out chain, which made
// kubelet liveness probes kill the validator pod. That made
// chain participation effectively all-or-nothing per validator.
// Now: validators participate per-plugin, opt-in.
if errors.Is(err, vms.ErrNotFound) {
m.Log.Info("chain VM plugin not loaded — opting out of this chain",
log.Stringer("chainID", chainParams.ChainID),
log.Stringer("chainID", chainParams.ID),
log.String("chainAlias", chainAlias),
log.Stringer("vmID", chainParams.VMID),
)
sb.Bootstrapped(chainParams.ID)
return
}
m.Log.Warn("non-critical chain failed to initialize",
log.Stringer("chainID", chainParams.ChainID),
log.Stringer("chainID", chainParams.ID),
log.String("chainAlias", chainAlias),
log.Stringer("vmID", chainParams.VMID),
log.Err(err),
)
// Non-critical chain failed for a real reason (not missing
// plugin). Mark it as bootstrapped so it doesn't block the
// node-level health check, but DO register a per-chain failing
// health check so operators see something is wrong with a chain
// they did opt into.
sb.Bootstrapped(chainParams.ID)
healthCheckErr := fmt.Errorf("failed to create chain on net %s: %w", chainParams.ChainID, err)
err := m.Health.RegisterHealthCheck(
chainAlias,
health.CheckerFunc(func(context.Context) (interface{}, error) {
return nil, healthCheckErr
}),
chainParams.ChainID.String(),
)
if err != nil {
m.Log.Error("failed to register failing health check",
log.Stringer("chainID", chainParams.ChainID),
log.Stringer("chainID", chainParams.ID),
log.String("chainAlias", chainAlias),
log.Stringer("vmID", chainParams.VMID),
log.Err(err),
)
}
return
}
m.chainsLock.Lock()
m.chains[chainParams.ID] = chain
m.chainsLock.Unlock()
// Associate the newly created chain with its default alias
if err := m.Alias(chainParams.ID, chainParams.ID.String()); err != nil {
m.Log.Error("failed to alias the new chain with itself",
log.Stringer("chainID", chainParams.ChainID),
log.Stringer("chainID", chainParams.ID),
log.Stringer("vmID", chainParams.VMID),
log.Err(err),
)
}
// Notify those who registered to be notified when a new chain is created
m.notifyRegistrants(chain.Name, chain.Runtime, chain.VM)
// Register HTTP handlers for this chain if the VM supports it
m.Log.Info("checking if VM implements CreateHandlers",
log.Stringer("chainID", chainParams.ID),
log.String("vmType", fmt.Sprintf("%T", chain.VM)),
)
if vm, ok := chain.VM.(interface {
CreateHandlers(context.Context) (map[string]http.Handler, error)
}); ok {
m.Log.Info("VM implements CreateHandlers, calling it",
log.Stringer("chainID", chainParams.ID),
)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
handlers, err := vm.CreateHandlers(ctx)
if err != nil {
m.Log.Error("failed to create HTTP handlers",
log.Stringer("chainID", chainParams.ID),
log.Err(err),
)
} else {
// Register each handler with the HTTP server
for endpoint, handler := range handlers {
chainAlias := chainParams.ID.String()
// For C-Chain, also register under the "C" alias
if chainParams.ID == m.CChainID {
chainAlias = "C"
}
// The base is just "bc/<chainID>" and endpoint is "/rpc" or "/"
chainBase := fmt.Sprintf("bc/%s", chainAlias)
chainIDBase := fmt.Sprintf("bc/%s", chainParams.ID.String())
// AddRoute will build the full path as /ext/<base><endpoint>
m.Server.AddRoute(handler, chainBase, endpoint)
if chainAlias != chainParams.ID.String() {
m.Server.AddRoute(handler, chainIDBase, endpoint)
}
// Also register with chain name alias for user-friendly routing (e.g., /ext/bc/zoo/rpc)
if chainParams.Name != "" {
nameLower := strings.ToLower(chainParams.Name)
nameBase := fmt.Sprintf("bc/%s", nameLower)
m.Server.AddRoute(handler, nameBase, endpoint)
m.Log.Info("Registered HTTP handler with chain name",
log.String("chainName", nameLower),
log.Stringer("chainID", chainParams.ID),
log.String("base", nameBase),
log.String("endpoint", endpoint),
)
// Register standard chain aliases (uppercase single-letter)
for alias, name := range map[string]string{
"C": "C-Chain", "X": "X-Chain", "D": "D-Chain",
"P": "P-Chain", "Q": "Q-Chain", "T": "T-Chain",
} {
if strings.EqualFold(chainParams.Name, name) {
m.Server.AddRoute(handler, "bc/"+alias, endpoint)
m.Log.Info("Registered HTTP handler with chain alias",
log.String("alias", alias),
log.Stringer("chainID", chainParams.ID),
log.String("endpoint", endpoint),
)
}
}
}
m.Log.Info("Registered HTTP handler",
log.String("chainAlias", chainAlias),
log.Stringer("chainID", chainParams.ID),
log.String("base", chainBase),
log.String("endpoint", endpoint),
)
}
}
}
// Register chain with the router for message routing
if m.ManagerConfig.Router != nil {
routeCtx, routeCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer routeCancel()
m.ManagerConfig.Router.AddChain(routeCtx, chainParams.ID, chain.Handler)
}
// Register bootstrapped health checks after P chain has been added to
// chains.
//
// Note: Registering this after the chain has been tracked prevents a race
// condition between the health check and adding the first chain to
// the manager.
if chainParams.ID == constants.PlatformChainID {
if err := m.registerBootstrappedHealthChecks(); err != nil {
if chain.Engine != nil {
stopCtx, stopCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer stopCancel()
chain.Engine.StopWithError(stopCtx, err)
}
}
}
// Log prominent chain creation success message with endpoints
vmName := constants.VMName(chainParams.VMID)
chainAlias := m.PrimaryAliasOrDefault(chainParams.ID)
m.Log.Info("╔══════════════════════════════════════════════════════════════════╗")
m.Log.Info("║ CHAIN CREATED SUCCESSFULLY ║",
log.String("vmName", vmName),
log.String("chainAlias", chainAlias),
)
m.Log.Info("║ Chain ID:", log.Stringer("chainID", chainParams.ID))
m.Log.Info("║ VM ID:", log.Stringer("vmID", chainParams.VMID))
m.Log.Info("║ Network ID:", log.Stringer("chainID", chainParams.ChainID))
m.Log.Info("║ Endpoints available at:")
m.Log.Info("║ → /ext/bc/" + chainParams.ID.String())
if chainAlias != chainParams.ID.String() {
m.Log.Info("║ → /ext/bc/" + chainAlias)
}
m.Log.Info("╚══════════════════════════════════════════════════════════════════╝")
// Tell the chain to start processing messages.
// If the X, P, or C Chain panics, do not attempt to recover
if chain.Engine != nil {
startCtx, startCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer startCancel()
chain.Engine.Start(startCtx, !m.CriticalChains.Contains(chainParams.ID))
// Start a goroutine to monitor bootstrap completion and notify the chain
// This is required because the health check (m.Nets.Bootstrapping()) reports
// chains as not bootstrapped until sb.Bootstrapped(chainID) is called
go m.monitorBootstrap(chain.Engine, sb, chainParams.ID)
} else {
// DAG chains (X-Chain, Q-Chain) manage their own consensus and don't have
// a standard Engine. Mark them as bootstrapped immediately since the DAG
// engine was already started in createDAG.
m.Log.Info("DAG chain has no standard engine, marking as bootstrapped immediately",
log.Stringer("chainID", chainParams.ID))
sb.Bootstrapped(chainParams.ID)
}
}
// Create a chain
func (m *manager) buildChain(chainParams ChainParameters, sb nets.Net) (*chainInfo, error) {
if chainParams.ID != constants.PlatformChainID && chainParams.VMID == constants.PlatformVMID {
return nil, errCreatePlatformVM
}
// primaryAlias will be used by the chains created below
primaryAlias := m.PrimaryAliasOrDefault(chainParams.ID)
// Create this chain's data directory
chainDataDir := filepath.Join(m.ChainDataDir, chainParams.ID.String())
if err := os.MkdirAll(chainDataDir, perms.ReadWriteExecute); err != nil {
return nil, fmt.Errorf("error while creating chain data directory %w", err)
}
// Create the log and context of the chain
chainLog := m.Log // Use main log instead of creating chain-specific log
// Create metrics gatherer for this chain
// The coreth EVM expects metric.MultiGatherer, not a legacy registry type
m.Log.Info("Creating metrics gatherer", log.String("primaryAlias", primaryAlias))
chainMetricsGatherer := metrics.NewMultiGatherer()
// Create a registry and register it with the gatherer
chainMetricsReg, err := metrics.MakeAndRegister(chainMetricsGatherer, primaryAlias)
if err != nil {
return nil, fmt.Errorf("failed to create chain metrics: %w", err)
}
// Also register with the global gatherer for metrics collection
if err := m.linearGatherer.Register(primaryAlias, chainMetricsReg); err != nil {
m.Log.Warn("Failed to register chain metrics with global gatherer",
log.String("primaryAlias", primaryAlias),
log.Err(err),
)
}
m.Log.Info("Metrics gatherer created",
log.String("primaryAlias", primaryAlias),
log.Bool("isNil", chainMetricsGatherer == nil),
)
// Note: Using local consensus package which has different fields
// PublicKey needs to be []byte, not *bls.PublicKey
var pubKeyBytes []byte
if m.StakingBLSKey != nil && m.StakingBLSKey.PublicKey() != nil {
// BLS PublicKey serialization not yet wired
pubKeyBytes = nil
}
// Create warp signer for this chain using the node's BLS key
warpSigner := createWarpSigner(m.StakingBLSKey, m.NetworkID, chainParams.ID)
// Create per-chain shared memory for cross-chain atomic operations.
// This is required by coreth's atomic VM for import/export transactions.
var chainSharedMemory atomic.SharedMemory
if m.AtomicMemory != nil {
chainSharedMemory = m.AtomicMemory.NewSharedMemory(chainParams.ID)
}
chainRuntime := &runtime.Runtime{
NetworkID: m.NetworkID,
ChainID: chainParams.ID,
NodeID: m.NodeID,
PublicKey: pubKeyBytes,
XChainID: m.XChainID,
CChainID: m.CChainID,
UTXOAssetID: m.UTXOAssetID,
ChainDataDir: chainDataDir,
BCLookup: m,
ValidatorState: getValidatorState(m.validatorState),
SharedMemory: chainSharedMemory,
Metrics: chainMetricsGatherer,
Log: chainLog,
WarpSigner: warpSigner,
}
// Get a factory for the vm we want to use on our chain
m.Log.Info("Getting VM factory", log.Stringer("vmID", chainParams.VMID))
vmFactory, err := m.VMManager.GetFactory(context.Background(), chainParams.VMID)
if err != nil {
// Check if this is a VM not found error - if so, add to pending chains for hot-loading
if errors.Is(err, vms.ErrNotFound) {
m.pendingVMChainsLock.Lock()
m.pendingVMChains[chainParams.VMID] = append(m.pendingVMChains[chainParams.VMID], chainParams)
m.pendingVMChainsLock.Unlock()
m.Log.Warn("VM not found - chain queued for hot-loading",
log.Stringer("vmID", chainParams.VMID),
log.Stringer("chainID", chainParams.ID),
)
return nil, fmt.Errorf("VM %s not found (chain queued for hot-loading): %w", chainParams.VMID, err)
}
m.Log.Error("Failed to get VM factory", log.Stringer("vmID", chainParams.VMID), log.Err(err))
return nil, fmt.Errorf("error while getting vmFactory: %w", err)
}
m.Log.Info("Got VM factory successfully")
// Create the chain
vmImpl, err := vmFactory.New(chainLog)
if err != nil {
return nil, fmt.Errorf("error while creating vm for chain %s: %w", chainParams.ID, err)
}
chainFxs := make([]*vm.Fx, len(chainParams.FxIDs))
for i, fxID := range chainParams.FxIDs {
fxFactory, ok := fxs[fxID]
if !ok {
return nil, fmt.Errorf("fx %s not found", fxID)
}
chainFxs[i] = &vm.Fx{
ID: fxID,
Fx: fxFactory.New(),
}
}
m.Log.Info("DEBUG: About to check VM type", log.Stringer("chainID", chainParams.ID), log.String("vmType", fmt.Sprintf("%T", vmImpl)))
var createdChain *chainInfo
switch vmTyped := vmImpl.(type) {
// DAG VM support - for X-Chain and Q-Chain
case interface{ GetEngine() consensusdag.Engine }:
m.Log.Info("detected DAG VM with GetEngine()",
log.Stringer("chainID", chainParams.ID),
)
createdChain, err = m.createDAG(chainRuntime, chainParams, vmTyped, chainFxs)
if err != nil {
return nil, fmt.Errorf("error creating DAG chain: %w", err)
}
case chain.ChainVM:
beacons := m.Validators
if chainParams.ID == constants.PlatformChainID {
beacons = chainParams.CustomBeacons
}
// In skip-bootstrap mode, use empty beacons for all chains
// This enables single-node development mode
if m.SkipBootstrap {
beacons = &emptyValidatorManager{}
m.Log.Info("skip-bootstrap enabled - using empty beacons for single-node mode")
}
// Note: For linear chains, the consensus engine uses networkGossiper
// which samples validators from m.Net (network's validator manager).
// The validator manager (n.vdrs) is populated by PlatformVM during
// its initialization via state.initValidatorSets(). Beacons are not
// directly used here but are available for future beacon-based bootstrap.
_ = beacons
// Create simple linear chain with basic consensus engine
m.Log.Info("creating linear chain", log.Stringer("chainID", chainRuntime.ChainID))
// Initialize the VM before creating the chain
// Get chain configuration
chainConfig, err := m.getChainConfig(chainParams.ID)
if err != nil {
m.Log.Warn("failed to get chain config, using empty config",
log.Stringer("chainID", chainParams.ID),
log.Err(err))
chainConfig = ChainConfig{}
}
// Get chain alias for database directory naming
linearChainAlias := chainParams.ID.String()
if aliases, _ := m.Aliases(chainParams.ID); len(aliases) > 0 {
linearChainAlias = aliases[0] // Use first alias (e.g., "P", "C")
}
// Get VM database from chain database manager
// Get VM database from chain database manager
vmDB, err := m.chainDBManager.GetVMDatabase(chainParams.ID, linearChainAlias)
if err != nil {
return nil, fmt.Errorf("failed to get database for chain %s: %w", chainParams.ID, err)
}
// Create message channel for VM-to-Engine communication
toEngine := make(chan vm.Message, 1)
// Convert []*vm.Fx to []interface{}
fxsInterface := make([]interface{}, len(chainFxs))
for i, fx := range chainFxs {
fxsInterface[i] = fx
}
// Initialize the VM if it supports the Initialize interface
// Inject automining config for dev mode (applies to C-Chain/coreth only),
// then stamp the chain-wide SecurityProfile pin into the C-Chain
// JSON config (F118) so the rpcchainvm plugin can resolve the
// profile on its side of the boundary.
vmConfigBytes := m.injectAutominingConfig(chainParams.VMID, chainConfig.Config)
vmConfigBytes = m.injectSecurityProfileConfig(chainParams.VMID, vmConfigBytes)
m.Log.Info("initializing VM", log.Stringer("chainID", chainParams.ID))
initCtx, initCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer initCancel()
err = vmTyped.Initialize(
initCtx,
vm.Init{
Runtime: chainRuntime,
DB: vmDB,
Log: chainLog,
Genesis: chainParams.GenesisData,
Upgrade: chainConfig.Upgrade,
Config: vmConfigBytes,
ToEngine: toEngine,
Fx: fxsInterface,
Sender: nil, // appSender - not needed for simple VMs
},
)
if err != nil {
m.Log.Error("VM initialization failed",
log.Stringer("chainID", chainParams.ID),
log.Err(err))
return nil, fmt.Errorf("failed to initialize VM: %w", err)
}
m.Log.Info("VM initialized successfully", log.Stringer("chainID", chainParams.ID))
// Transition VM to normal operation after initialization
// For genesis-based networks with pre-configured validators, this is required
// to make the VM APIs available immediately
if stateVM, ok := vmTyped.(interface {
SetState(context.Context, uint32) error
}); ok {
m.Log.Info("transitioning VM to normal operation",
log.Stringer("chainID", chainParams.ID))
stateCtx, stateCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer stateCancel()
if err := stateVM.SetState(stateCtx, uint32(vm.Ready)); err != nil {
m.Log.Error("failed to transition VM to normal operation",
log.Stringer("chainID", chainParams.ID),
log.Err(err))
return nil, fmt.Errorf("failed to transition VM to normal operation: %w", err)
}
}
// Create integrated consensus engine - the ONE right way to set up chain consensus
// This consolidates: engine creation, emitter wiring, VM registration
var blockBuilder consensuschain.BlockBuilder
if bb, ok := vmTyped.(consensuschain.BlockBuilder); ok {
blockBuilder = bb
m.Log.Info("registered VM with consensus engine for block building",
log.Stringer("chainID", chainParams.ID))
} else {
m.Log.Warn("VM does not implement BlockBuilder interface, block building disabled",
log.Stringer("chainID", chainParams.ID))
}
// For native/primary network chains (P/C/X/Q/A/B/T/Z etc.), use PrimaryNetworkID for validator lookups.
// Native chains all have IDs with first 31 bytes zero, last byte is the chain letter (e.g., 'P', 'C').
// Validators are registered under constants.PrimaryNetworkID (ids.Empty), not individual chain IDs.
// For L1/net chains, use the net's validator set ID (chainParams.ChainID).
networkID := chainParams.ChainID
isNative := ids.IsNativeChain(chainParams.ID)
if isNative {
// Native chains (P, C, X, Q, A, B, T, Z, G, I, K) use PrimaryNetworkID for validator lookups
networkID = constants.PrimaryNetworkID
}
if m.Validators != nil && networkID != constants.PrimaryNetworkID {
if m.Validators.Count(networkID) == 0 {
m.Log.Warn("no validators found for network ID; falling back to primary network validators",
log.Stringer("chainID", chainParams.ID),
log.Stringer("networkID", networkID),
log.Stringer("fallbackNetworkID", constants.PrimaryNetworkID),
)
networkID = constants.PrimaryNetworkID
}
}
m.Log.Info("[CONSENSUS DEBUG] Creating consensus engine for chain",
log.Stringer("chainID", chainParams.ID),
log.Stringer("chainParams.ChainID", chainParams.ChainID),
log.Bool("isNativeChain", isNative),
log.Stringer("networkIDForValidators", networkID),
log.Stringer("PrimaryNetworkID", constants.PrimaryNetworkID),
)
// Choose consensus parameters based on mode:
// - Single-node (--dev, sybil protection disabled): K=1, self-voting
// - Multi-node (normal): K=3, 2/3 threshold
var consensusParams consensusconfig.Parameters
if !m.SybilProtectionEnabled {
consensusParams = consensusconfig.Parameters{
K: 1,
Alpha: 1.0,
AlphaPreference: 1,
AlphaConfidence: 1,
Beta: 1,
ConcurrentPolls: 1,
OptimalProcessing: 1,
MaxOutstandingItems: 256,
MaxItemProcessingTime: 30 * time.Second,
}
} else {
consensusParams = consensusconfig.LocalParams()
}
consensusEngine := consensuschain.NewRuntime(consensuschain.NetworkConfig{
ChainID: chainParams.ID,
NetworkID: networkID,
NodeID: m.NodeID,
Validators: m.Validators, // CRITICAL: Pass validator sampler for k-peer polling
Logger: m.Log,
Gossiper: &networkGossiper{net: m.Net, msgCreator: m.MsgCreator, networkID: networkID},
VM: blockBuilder,
Params: &consensusParams,
})
// Start the consensus engine
engineStartCtx, engineStartCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer engineStartCancel()
if err := consensusEngine.Start(engineStartCtx, true); err != nil {
m.Log.Error("failed to start consensus engine",
log.Stringer("chainID", chainParams.ID),
log.Err(err))
return nil, fmt.Errorf("failed to start consensus engine: %w", err)
}
m.Log.Info("consensus engine started with Lux consensus (Photon → Wave → Focus)",
log.Stringer("chainID", chainParams.ID))
if blockBuilder != nil {
syncCtx, syncCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer syncCancel()
lastAcceptedID, height, err := consensuschain.SyncStateFromVM(syncCtx, blockBuilder, consensusEngine.Transitive)
if err != nil {
m.Log.Warn("failed to sync consensus state from VM",
log.Stringer("chainID", chainParams.ID),
log.Stringer("lastAccepted", lastAcceptedID),
log.Uint64("height", height),
log.Err(err))
} else {
m.Log.Info("synced consensus state from VM",
log.Stringer("chainID", chainParams.ID),
log.Stringer("lastAccepted", lastAcceptedID),
log.Uint64("height", height))
}
}
// Bridge VM's WaitForEvent to toEngine channel.
// This is the critical missing piece: ForwardVMNotifications reads from toEngine,
// but nothing was writing to it! This goroutine calls WaitForEvent on the VM
// and writes the result to toEngine, which ForwardVMNotifications then reads
// and forwards to the consensus engine via Notify().
go func() {
ctx := context.Background()
for {
// Call WaitForEvent on the VM - this blocks until there are pending txs
// or staker changes that should trigger block building
msg, err := vmTyped.WaitForEvent(ctx)
if err != nil {
if ctx.Err() != nil {
// Context cancelled, exit gracefully
return
}
m.Log.Warn("WaitForEvent error, retrying",
log.Stringer("chainID", chainParams.ID),
log.Err(err))
time.Sleep(time.Second)
continue
}
// WaitForEvent now returns vm.Message directly
m.Log.Debug("[VM NOTIFICATION] WaitForEvent returned",
log.Stringer("chainID", chainParams.ID),
log.Uint32("messageType", uint32(msg.Type)))
toEngine <- msg
m.Log.Debug("[VM NOTIFICATION] Sent to toEngine channel",
log.Stringer("chainID", chainParams.ID),
log.Uint32("msgType", uint32(msg.Type)))
}
}()
// Forward VM notifications to consensus (single goroutine)
go consensusEngine.ForwardVMNotifications(toEngine)
chainName := chainParams.Name
if chainName == "" {
chainName = chainRuntime.ChainID.String()
}
createdChain = &chainInfo{
Name: chainName,
VMID: chainParams.VMID,
Runtime: chainRuntime,
VM: vmTyped, // Use the real VM directly
Engine: consensusEngine, // Use real consensus engine directly
Handler: newBlockHandler(vmTyped, m.Log, consensusEngine, m.Net, m.MsgCreator, chainParams.ID, networkID),
}
default:
return nil, fmt.Errorf("unsupported VM type: %T", vmImpl)
}
vmGatherer, err := m.getOrMakeVMGatherer(chainParams.VMID)
if err != nil {
return nil, err
}
_ = vmGatherer
return createdChain, nil
}
func (m *manager) AddRegistrant(r Registrant) {
m.registrants = append(m.registrants, r)
}
// dagVMAdapter adapts a DAG VM to interfaces.VM for HTTP handler registration
type dagVMAdapter struct {
underlying interface{}
}
func (v *dagVMAdapter) CreateHandlers(ctx context.Context) (map[string]http.Handler, error) {
if h, ok := v.underlying.(interface {
CreateHandlers(context.Context) (map[string]http.Handler, error)
}); ok {
return h.CreateHandlers(ctx)
}
return map[string]http.Handler{}, nil
}
func (v *dagVMAdapter) CreateStaticHandlers(ctx context.Context) (map[string]http.Handler, error) {
if h, ok := v.underlying.(interface {
CreateStaticHandlers(context.Context) (map[string]http.Handler, error)
}); ok {
return h.CreateStaticHandlers(ctx)
}
return map[string]http.Handler{}, nil
}
func (v *dagVMAdapter) HealthCheck(ctx context.Context) (interface{}, error) {
return map[string]interface{}{"healthy": true}, nil
}
func (v *dagVMAdapter) NewHTTPHandler(ctx context.Context) (http.Handler, error) {
return nil, nil
}
func (v *dagVMAdapter) SetState(ctx context.Context, state vm.State) error {
if s, ok := v.underlying.(interface {
SetState(context.Context, uint32) error
}); ok {
return s.SetState(ctx, uint32(state))
}
return nil
}
func (v *dagVMAdapter) Shutdown(ctx context.Context) error {
if s, ok := v.underlying.(interface {
Shutdown(context.Context) error
}); ok {
return s.Shutdown(ctx)
}
return nil
}
func (v *dagVMAdapter) Version(ctx context.Context) (string, error) {
return "1.0.0", nil
}
func (v *dagVMAdapter) Initialize(
ctx context.Context,
chainRuntime *runtime.Runtime,
dbMgr dbmanager.Manager,
genesisBytes []byte,
upgradeBytes []byte,
configBytes []byte,
toEngine chan<- vm.Message,
fxs []*vm.Fx,
appSender interface{},
) error {
return nil // DAG VMs are pre-initialized
}
// createDAG creates a DAG chain (X-Chain, Q-Chain) using the VM's DAG engine
func (m *manager) createDAG(
rt *runtime.Runtime,
chainParams ChainParameters,
vmImpl interface{},
fxs []*vm.Fx,
) (*chainInfo, error) {
// Type assert to get GetEngine() method from exchangevm/qvm
dagVM, ok := vmImpl.(interface{ GetEngine() consensusdag.Engine })
if !ok {
return nil, fmt.Errorf("VM does not implement GetEngine() for DAG consensus")
}
m.Log.Info("creating DAG chain",
log.Stringer("chainID", chainParams.ID),
log.String("vmID", chainParams.VMID.String()),
)
// Register chain aliases early so the VM's address parser can resolve them.
// The "X" prefix in addresses like "X-dev1..." must resolve to the actual blockchain ID.
if err := m.Alias(chainParams.ID, chainParams.ID.String()); err != nil {
m.Log.Warn("failed to alias chain with itself", log.Err(err))
}
if strings.EqualFold(chainParams.Name, "X-Chain") {
_ = m.Alias(chainParams.ID, "X")
} else if strings.EqualFold(chainParams.Name, "Q-Chain") {
_ = m.Alias(chainParams.ID, "Q")
}
// Get chain configuration
chainConfig, err := m.getChainConfig(chainParams.ID)
if err != nil {
m.Log.Warn("failed to get chain config, using empty config",
log.Stringer("chainID", chainParams.ID),
log.Err(err))
chainConfig = ChainConfig{}
}
// Inject automining config for dev mode (applies to C-Chain/coreth only)
chainConfig.Config = m.injectAutominingConfig(chainParams.VMID, chainConfig.Config)
// Get chain alias for database directory naming
chainAlias := chainParams.ID.String()
if aliases, _ := m.Aliases(chainParams.ID); len(aliases) > 0 {
chainAlias = aliases[0] // Use first alias (e.g., "X", "Q")
}
// Get VM database from chain database manager
// In isolated mode, each chain gets its own ZapDB
// In legacy mode, uses prefixdb on shared database
vmDB, err := m.chainDBManager.GetVMDatabase(chainParams.ID, chainAlias)
if err != nil {
return nil, fmt.Errorf("failed to get database for chain %s: %w", chainParams.ID, err)
}
// Create a context for VM initialization with timeout
initCtx, cancelInit := context.WithTimeout(context.Background(), 30*time.Second)
defer cancelInit() // Ensure cleanup on function exit
// Initialize VM if it supports Initialize
// Try multiple Initialize signatures since VMs may have different interfaces
vmInitialized := false
// Try QVM Initialize signature (uses consensus/core types)
if initVM, ok := vmImpl.(interface {
Initialize(
ctx context.Context,
chainRuntime interface{},
db database.Database,
genesisBytes []byte,
upgradeBytes []byte,
configBytes []byte,
toEngine chan<- vm.Message,
fxs []*vm.Fx,
appSender warp.Sender,
) error
}); ok {
toEngine := make(chan vm.Message, 1)
err := initVM.Initialize(
initCtx,
rt,
vmDB,
chainParams.GenesisData,
chainConfig.Upgrade,
chainConfig.Config,
toEngine,
fxs,
&noopWarpSender{}, // Simple no-op for non-warp VMs
)
if err != nil {
m.Log.Warn("QVM-style initialization failed", log.Stringer("chainID", chainParams.ID), log.Err(err))
} else {
m.Log.Info("QVM initialized successfully", log.Stringer("chainID", chainParams.ID))
vmInitialized = true
}
}
// Try ExchangeVM Initialize signature (uses interface{} types for flexibility)
if !vmInitialized {
if initVM, ok := vmImpl.(interface {
Initialize(
ctx context.Context,
chainRuntime interface{},
dbManager interface{},
genesisBytes []byte,
upgradeBytes []byte,
configBytes []byte,
toEngine chan<- interface{},
fxs []interface{},
appSender interface{},
) error
}); ok {
toEngine := make(chan interface{}, 1)
// Convert fxs to []interface{}
fxsInterface := make([]interface{}, len(fxs))
for i, fx := range fxs {
fxsInterface[i] = fx
}
err := initVM.Initialize(
initCtx,
rt,
vmDB,
chainParams.GenesisData,
chainConfig.Upgrade,
chainConfig.Config,
toEngine,
fxsInterface,
&noopWarpSender{}, // Implements p2p.Sender interface
)
if err != nil {
m.Log.Warn("ExchangeVM-style initialization failed", log.Stringer("chainID", chainParams.ID), log.Err(err))
} else {
m.Log.Info("ExchangeVM initialized successfully", log.Stringer("chainID", chainParams.ID))
vmInitialized = true
}
}
}
// Try vmcore.Init struct-based Initialize (used by exchangevm)
if !vmInitialized {
if initVM, ok := vmImpl.(interface {
Initialize(context.Context, vm.Init) error
}); ok {
toEngine := make(chan vm.Message, 1)
// Convert []*vm.Fx to []any for vm.Init.Fx field
fxAny := make([]any, len(fxs))
for i, fx := range fxs {
fxAny[i] = fx
}
err := initVM.Initialize(initCtx, vm.Init{
Runtime: rt,
DB: vmDB,
Genesis: chainParams.GenesisData,
Upgrade: chainConfig.Upgrade,
Config: chainConfig.Config,
ToEngine: toEngine,
Fx: fxAny,
})
if err != nil {
m.Log.Warn("vmcore.Init-style initialization failed",
log.Stringer("chainID", chainParams.ID), log.Err(err))
} else {
m.Log.Info("ExchangeVM initialized via vmcore.Init",
log.Stringer("chainID", chainParams.ID))
vmInitialized = true
}
}
}
// Linearize the DAG chain (required for X-Chain).
// This transitions the chain from DAG mode to linear block mode.
if vmInitialized {
if linearVM, ok := vmImpl.(interface {
Linearize(context.Context, ids.ID, chan<- vm.Message) error
}); ok {
toEngine := make(chan vm.Message, 1)
if err := linearVM.Linearize(initCtx, ids.Empty, toEngine); err != nil {
m.Log.Warn("failed to linearize DAG chain", log.Stringer("chainID", chainParams.ID), log.Err(err))
} else {
m.Log.Info("DAG chain linearized successfully", log.Stringer("chainID", chainParams.ID))
}
}
}
// Only transition VM to normal operation if initialization succeeded
if vmInitialized {
if stateVM, ok := vmImpl.(interface {
SetState(context.Context, uint32) error
}); ok {
if err := stateVM.SetState(initCtx, uint32(vm.Ready)); err != nil {
m.Log.Warn("failed to transition VM to normal op", log.Stringer("chainID", chainParams.ID), log.Err(err))
}
}
}
// Get and start the DAG engine
dagEngine := dagVM.GetEngine()
if starter, ok := dagEngine.(interface {
Start(context.Context, uint32) error
}); ok {
if err := starter.Start(context.Background(), 0); err != nil {
return nil, fmt.Errorf("failed to start DAG engine: %w", err)
}
}
m.Log.Info("DAG chain created successfully",
log.Stringer("chainID", chainParams.ID),
log.String("status", "using native DAG consensus"),
)
// Register HTTP handlers for DAG VMs (exchangevm, qvm, etc.)
adapter := &dagVMAdapter{underlying: vmImpl}
dagHandlerCtx, dagHandlerCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer dagHandlerCancel()
handlers, err := adapter.CreateHandlers(dagHandlerCtx)
if err != nil {
m.Log.Warn("failed to create HTTP handlers for DAG chain",
log.Stringer("chainID", chainParams.ID),
log.Err(err),
)
} else if len(handlers) > 0 {
chainIDStr := chainParams.ID.String()
for endpoint, handler := range handlers {
m.Server.AddRoute(handler, "bc/"+chainIDStr, endpoint)
// Register name alias (e.g., "x-chain")
if chainParams.Name != "" {
m.Server.AddRoute(handler, "bc/"+strings.ToLower(chainParams.Name), endpoint)
}
// Register standard single-letter alias
for alias, name := range map[string]string{
"X": "X-Chain", "Q": "Q-Chain",
} {
if strings.EqualFold(chainParams.Name, name) {
m.Server.AddRoute(handler, "bc/"+alias, endpoint)
m.Log.Info("Registered DAG chain HTTP handler",
log.String("alias", alias),
log.Stringer("chainID", chainParams.ID),
log.String("endpoint", endpoint),
)
}
}
}
}
dagName := chainParams.Name
if dagName == "" {
dagName = chainParams.ID.String()
}
return &chainInfo{
Name: dagName,
VMID: chainParams.VMID,
Runtime: rt,
VM: adapter,
Handler: &noopHandler{},
}, nil
}
// errBootstrapTimeout is returned when a chain fails to bootstrap within the timeout period
var errBootstrapTimeout = errors.New("chain failed to bootstrap within timeout")
// monitorBootstrap monitors when a chain finishes bootstrapping and notifies the chain.
// This is critical for health checks because the health check queries m.Nets.Bootstrapping()
// which returns chains that have chains still in bootstrapping state. Without this notification,
// the health check would permanently report "chains not bootstrapped".
//
// IMPORTANT: If bootstrap times out, the chain is NOT marked as bootstrapped. This ensures
// real bootstrap failures are surfaced rather than masked by forcing a "ready" state.
func (m *manager) monitorBootstrap(engine Engine, sb nets.Net, chainID ids.ID) {
// Check if the engine supports IsBootstrapped
type bootstrapChecker interface {
IsBootstrapped() bool
}
checker, ok := engine.(bootstrapChecker)
if !ok {
// Engine doesn't support IsBootstrapped, immediately mark as bootstrapped
// This is safe because if we can't check, we assume the chain is ready
m.Log.Info("engine does not support IsBootstrapped, marking chain as bootstrapped",
log.Stringer("chainID", chainID))
sb.Bootstrapped(chainID)
return
}
// Poll the engine until it reports bootstrapped
// Use a short initial delay to let the engine start up, then poll regularly
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
// Set a reasonable timeout (5 minutes for local networks)
// After timeout, we do NOT mark as bootstrapped - this is a real failure
timeout := time.NewTimer(5 * time.Minute)
defer timeout.Stop()
// Track polling count for diagnostics
pollCount := 0
for {
select {
case <-ticker.C:
pollCount++
if checker.IsBootstrapped() {
m.Log.Info("chain finished bootstrapping, notifying chain",
log.Stringer("chainID", chainID),
log.Int("pollCount", pollCount))
sb.Bootstrapped(chainID)
return
}
case <-timeout.C:
// Timeout reached - this is a real bootstrap failure
// DO NOT mark as bootstrapped - this masks real failures and causes unpredictable behavior
m.Log.Error("chain bootstrap timeout - chain NOT marked as bootstrapped",
log.Stringer("chainID", chainID),
log.Int("pollCount", pollCount),
log.String("lastState", "still bootstrapping after 5 minutes"),
log.Err(errBootstrapTimeout))
// Stop the engine with the bootstrap timeout error
// This ensures the chain is properly marked as failed
if err := engine.StopWithError(context.Background(), errBootstrapTimeout); err != nil {
m.Log.Error("failed to stop engine after bootstrap timeout",
log.Stringer("chainID", chainID),
log.Err(err))
}
// Register a health check that reports the bootstrap failure
chainAlias := m.PrimaryAliasOrDefault(chainID)
healthErr := m.Health.RegisterHealthCheck(
chainAlias+"-bootstrap",
health.CheckerFunc(func(context.Context) (interface{}, error) {
return map[string]interface{}{
"chainID": chainID.String(),
"error": "bootstrap timeout",
"pollCount": pollCount,
}, errBootstrapTimeout
}),
health.ApplicationTag,
)
if healthErr != nil {
m.Log.Error("failed to register bootstrap timeout health check",
log.Stringer("chainID", chainID),
log.Err(healthErr))
}
return
case <-m.chainCreatorShutdownCh:
// Manager is shutting down
return
}
}
}
func (m *manager) IsBootstrapped(id ids.ID) bool {
m.chainsLock.Lock()
_, exists := m.chains[id]
m.chainsLock.Unlock()
if !exists {
return false
}
// Bootstrapped chains start in NormalOp
return true
}
func (m *manager) GetChains() []ChainInfo {
m.chainsLock.Lock()
defer m.chainsLock.Unlock()
result := make([]ChainInfo, 0, len(m.chains))
for id, info := range m.chains {
result = append(result, ChainInfo{
ID: id,
Name: info.Name,
VMID: info.VMID,
Bootstrapped: true,
})
}
return result
}
func (m *manager) registerBootstrappedHealthChecks() error {
bootstrappedCheck := health.CheckerFunc(func(context.Context) (interface{}, error) {
if chainIDs := m.Nets.Bootstrapping(); len(chainIDs) != 0 {
return chainIDs, errNotBootstrapped
}
return []ids.ID{}, nil
})
if err := m.Health.RegisterReadinessCheck("bootstrapped", bootstrappedCheck, health.ApplicationTag); err != nil {
return fmt.Errorf("couldn't register bootstrapped readiness check: %w", err)
}
if err := m.Health.RegisterHealthCheck("bootstrapped", bootstrappedCheck, health.ApplicationTag); err != nil {
return fmt.Errorf("couldn't register bootstrapped health check: %w", err)
}
// We should only report unhealthy if the node is partially syncing the
// primary network and is a validator.
if !m.PartialSyncPrimaryNetwork {
return nil
}
partialSyncCheck := health.CheckerFunc(func(context.Context) (interface{}, error) {
// Note: The health check is skipped during bootstrapping to allow a
// node to sync the network even if it was previously a validator.
if !m.IsBootstrapped(constants.PlatformChainID) {
return "node is currently bootstrapping", nil
}
if _, ok := m.Validators.GetValidator(constants.PrimaryNetworkID, m.NodeID); !ok {
return "node is not a primary network validator", nil
}
m.Log.Warn("node is a primary network validator",
log.Err(errPartialSyncAsAValidator),
)
return "node is a primary network validator", errPartialSyncAsAValidator
})
if err := m.Health.RegisterHealthCheck("validation", partialSyncCheck, health.ApplicationTag); err != nil {
return fmt.Errorf("couldn't register validation health check: %w", err)
}
return nil
}
// Starts chain creation loop to process queued chains
func (m *manager) StartChainCreator(platformParams ChainParameters) error {
// Add the P-Chain to the Primary Network
sb, _ := m.Nets.GetOrCreate(constants.PrimaryNetworkID)
sb.AddChain(platformParams.ID)
// The P-chain is created synchronously to ensure that `VM.Initialize` has
// finished before returning from this function. This is required because
// the P-chain initializes state that the rest of the node initialization
// depends on.
m.createChain(platformParams)
m.Log.Info("starting chain creator")
m.chainCreatorExited.Add(1)
go func() { close(m.unblockChainCreatorCh) }()
go m.dispatchChainCreator()
return nil
}
func (m *manager) dispatchChainCreator() {
defer m.chainCreatorExited.Done()
select {
// This channel will be closed when Shutdown is called on the manager.
case <-m.chainCreatorShutdownCh:
return
case <-m.unblockChainCreatorCh:
}
// Handle chain creations
for {
// Get the next chain we should create.
// Dequeue waits until an element is pushed, so this is not
// busy-looping.
chainParams, ok := m.chainsQueue.PopLeft()
if !ok { // queue is closed, return directly
return
}
m.createChain(chainParams)
}
}
// PrimaryAliasOrDefault returns the primary alias for a chain, or the chain ID if no alias exists
func (m *manager) PrimaryAliasOrDefault(chainID ids.ID) string {
alias, err := m.PrimaryAlias(chainID)
if err != nil {
// Return chain ID as string if no alias found
return chainID.String()
}
return alias
}
// Shutdown stops all the chains
func (m *manager) Shutdown() {
m.Log.Info("shutting down chain manager")
m.chainsQueue.Close()
close(m.chainCreatorShutdownCh)
m.chainCreatorExited.Wait()
// Router doesn't have Shutdown method in consensus package
}
// LookupVM returns the ID of the VM associated with an alias
func (m *manager) LookupVM(alias string) (ids.ID, error) {
return m.VMManager.Lookup(context.Background(), alias)
}
// RetryPendingChains re-queues chains that were waiting for the specified VM.
// This is called when a VM is hot-loaded via admin.loadVMs.
// Returns the number of chains that were re-queued.
func (m *manager) RetryPendingChains(vmID ids.ID) int {
m.pendingVMChainsLock.Lock()
pendingChains, ok := m.pendingVMChains[vmID]
if ok {
delete(m.pendingVMChains, vmID)
}
m.pendingVMChainsLock.Unlock()
if !ok || len(pendingChains) == 0 {
return 0
}
// Re-queue all pending chains for this VM
for _, chainParams := range pendingChains {
m.Log.Info("Re-queuing chain after VM hot-load",
log.Stringer("vmID", vmID),
log.Stringer("chainID", chainParams.ID),
)
m.chainsQueue.PushRight(chainParams)
}
return len(pendingChains)
}
// GetPendingChains returns the chain parameters waiting for a VM to be loaded.
func (m *manager) GetPendingChains(vmID ids.ID) []ChainParameters {
m.pendingVMChainsLock.RLock()
defer m.pendingVMChainsLock.RUnlock()
pendingChains, ok := m.pendingVMChains[vmID]
if !ok {
return nil
}
// Return a copy to avoid race conditions
result := make([]ChainParameters, len(pendingChains))
copy(result, pendingChains)
return result
}
// Notify registrants [those who want to know about the creation of chains]
// that the specified chain has been created
func (m *manager) notifyRegistrants(name string, rt *runtime.Runtime, vmImpl interface{}) {
for _, registrant := range m.registrants {
if coreVM, ok := vmImpl.(vm.VM); ok {
registrant.RegisterChain(name, rt, coreVM)
}
}
}
// getChainConfig returns value of a entry by looking at ID key and alias key
// it first searches ID key, then falls back to it's corresponding primary alias
func (m *manager) getChainConfig(id ids.ID) (ChainConfig, error) {
if val, ok := m.ManagerConfig.ChainConfigs[id.String()]; ok {
return val, nil
}
aliases, err := m.Aliases(id)
if err != nil {
return ChainConfig{}, err
}
for _, alias := range aliases {
if val, ok := m.ManagerConfig.ChainConfigs[alias]; ok {
return val, nil
}
}
return ChainConfig{}, nil
}
// injectSecurityProfileConfig stamps the chain-wide ChainSecurityProfile
// pin into the C-Chain JSON config bytes so the coreth plugin VM can
// resolve the profile on its side of the rpcchainvm boundary. Closes
// red-team finding F118.
//
// The pin form is intentionally minimal — ProfileID byte + ProfileHash
// hex — so this manager package does not need to round-trip the full
// ChainSecurityProfile struct. The plugin VM calls
// consensusconfig.ProfileByID + ComputeHash and refuses the chain if the
// pinned hash does not match the live canonical content. Forked binaries
// that swap canonical profile content fail the hash compare; legitimate
// upgrades land via a new ProfileID and ProfileHash pair.
//
// Only applies to C-Chain (EVMID); other VMs are passed through.
func (m *manager) injectSecurityProfileConfig(vmID ids.ID, configBytes []byte) []byte {
if m.SecurityProfile == nil {
return configBytes
}
if vmID != constants.EVMID {
return configBytes
}
// Parse existing config or create empty object.
var cfg map[string]interface{}
if len(configBytes) > 0 {
if err := json.Unmarshal(configBytes, &cfg); err != nil {
m.Log.Warn("failed to parse C-Chain config for security profile injection, creating new config",
log.Err(err))
cfg = make(map[string]interface{})
}
} else {
cfg = make(map[string]interface{})
}
cfg["lux-security-profile"] = map[string]interface{}{
"profileID": m.SecurityProfile.ProfileID & 0xff,
"profileHashHex": fmt.Sprintf("%x", m.SecurityProfile.ProfileHash[:]),
}
out, err := json.Marshal(cfg)
if err != nil {
m.Log.Warn("failed to marshal C-Chain config after security profile injection", log.Err(err))
return configBytes
}
m.Log.Info("injected chain security profile pin into C-Chain plugin config",
log.Uint32("profileID", m.SecurityProfile.ProfileID),
log.String("profileName", m.SecurityProfile.ProfileName))
return out
}
// injectAutominingConfig modifies the config bytes to include enable-automining flag
// when dev mode automining is enabled. This is used for C-Chain (coreth) to enable
// anvil-like block production behavior.
// Only applies to VMs that use JSON config format (EVMID). VMs using binary codec
// config (ThresholdVM, ZKVM, etc.) are not modified.
func (m *manager) injectAutominingConfig(vmID ids.ID, configBytes []byte) []byte {
if !m.EnableAutomining {
return configBytes
}
// Only inject automining config for EVM-based chains (C-Chain)
// Other VMs like ThresholdVM, ZKVM use binary codec config format
if vmID != constants.EVMID {
return configBytes
}
// Parse existing config or create empty object
var config map[string]interface{}
if len(configBytes) > 0 {
if err := json.Unmarshal(configBytes, &config); err != nil {
// If we can't parse existing config, create new one with just automining
m.Log.Warn("failed to parse chain config for automining injection, creating new config",
log.Err(err))
config = make(map[string]interface{})
}
} else {
config = make(map[string]interface{})
}
// Inject enable-automining flag
config["enable-automining"] = true
// Inject skip-block-fee flag to allow block generation without requiring transaction fees
// This is necessary for dev mode APIs (eth_setBalance, eth_setStorageAt, evm_mine, etc.)
config["skip-block-fee"] = true
// Serialize back to JSON
modifiedBytes, err := json.Marshal(config)
if err != nil {
m.Log.Warn("failed to marshal modified chain config", log.Err(err))
return configBytes
}
m.Log.Info("injected enable-automining and skip-block-fee into chain config")
return modifiedBytes
}
func (m *manager) getOrMakeVMGatherer(vmID ids.ID) (metrics.MultiGatherer, error) {
vmGatherer, ok := m.vmGatherer[vmID]
if ok {
return vmGatherer, nil
}
vmName := constants.VMName(vmID)
// metric.AppendNamespace doesn't exist in current metric package
vmNamespace := vmName // Simplified - just use vmName directly
vmGatherer = metrics.NewLabelGatherer(ChainLabel)
err := m.Metrics.Register(
vmNamespace,
vmGatherer,
)
if err != nil {
return nil, err
}
m.vmGatherer[vmID] = vmGatherer
return vmGatherer, nil
}
// emptyValidatorManager implements validators.Manager with no validators
type emptyValidatorManager struct{}
func (e *emptyValidatorManager) GetValidator(chainID ids.ID, nodeID ids.NodeID) (*validators.GetValidatorOutput, bool) {
return nil, false
}
func (e *emptyValidatorManager) GetValidators(chainID ids.ID) (validators.Set, error) {
// Return nil for empty validator set since NewEmpty doesn't exist
return nil, nil
}
func (e *emptyValidatorManager) GetWeight(chainID ids.ID, nodeID ids.NodeID) uint64 {
return 0
}
func (e *emptyValidatorManager) GetCurrentHeight(context.Context) (uint64, error) {
return 0, nil
}
func (e *emptyValidatorManager) GetValidatorSet(ctx context.Context, height uint64, chainID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
return map[ids.NodeID]*validators.GetValidatorOutput{}, nil
}
func (e *emptyValidatorManager) GetChainHeight(ctx context.Context, chainID ids.ID) (uint64, error) {
return 0, nil
}
func (e *emptyValidatorManager) OnAcceptedBlockID(blkID ids.ID) {}
func (e *emptyValidatorManager) String() string {
return "empty validator manager"
}
func (e *emptyValidatorManager) TotalWeight(chainID ids.ID) (uint64, error) {
return 0, nil
}
func (e *emptyValidatorManager) GetLight(chainID ids.ID, nodeID ids.NodeID) uint64 {
return 0
}
func (e *emptyValidatorManager) TotalLight(chainID ids.ID) (uint64, error) {
return 0, nil
}
func (e *emptyValidatorManager) AddStaker(chainID ids.ID, nodeID ids.NodeID, publicKey []byte, txID ids.ID, light uint64) error {
return nil
}
func (e *emptyValidatorManager) AddWeight(chainID ids.ID, nodeID ids.NodeID, weight uint64) error {
return nil
}
func (e *emptyValidatorManager) RemoveWeight(chainID ids.ID, nodeID ids.NodeID, weight uint64) error {
return nil
}
func (e *emptyValidatorManager) GetMap(chainID ids.ID) map[ids.NodeID]*validators.GetValidatorOutput {
return nil
}
func (e *emptyValidatorManager) GetValidatorIDs(chainID ids.ID) []ids.NodeID {
return nil
}
func (e *emptyValidatorManager) NumValidators(chainID ids.ID) int {
return 0
}
func (e *emptyValidatorManager) NumNets() int {
return 0
}
func (e *emptyValidatorManager) SubsetWeight(chainID ids.ID, nodeIDs set.Set[ids.NodeID]) (uint64, error) {
return 0, nil
}
func (e *emptyValidatorManager) Sample(chainID ids.ID, size int) ([]ids.NodeID, error) {
return nil, nil
}
func (e *emptyValidatorManager) Count(chainID ids.ID) int {
return 0
}
func (e *emptyValidatorManager) RegisterCallbackListener(listener validators.ManagerCallbackListener) {
}
func (e *emptyValidatorManager) RegisterSetCallbackListener(chainID ids.ID, listener validators.SetCallbackListener) {
}
func (e *emptyValidatorManager) GetCurrentValidators(ctx context.Context, height uint64, chainID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
return nil, nil
}
// blockHandler implements handler.Handler interface and processes incoming blocks
// This enables block propagation between validators
type blockHandler struct {
vm chain.ChainVM
logger log.Logger
engine *consensuschain.Runtime // Consensus engine for proper block handling
net network.Network // Network for sending Qbit responses
msgCreator message.OutboundMsgBuilder // Message creator for Qbit responses
chainID ids.ID // Chain ID for message routing
networkID ids.ID // Network ID for validator routing
// Context sync support - when a block fails verification due to missing context,
// we request the prerequisite blocks from the peer to catch up
pendingContext map[ids.ID]contextRequest // Map from blockID to pending context request
requestIDCounter uint32 // Counter for generating unique request IDs
maxContextBlocks int // Max context blocks to request/serve (default: 256)
contextRequestMu sync.Mutex // Protects pendingContext and requestIDCounter
// Qbit event buffering - when we receive a Qbit for a block we don't have yet,
// buffer the event and drain when the block arrives
pendingQbits map[ids.ID][]QbitEvent // Map from blockID to buffered Qbit events
pendingQbitMu sync.Mutex // Protects pendingQbits
}
// QbitEvent is the normalized internal representation of a received Qbit message.
// This is pure data - no VM calls, no Verify, no Accept derivation.
// Vote creation happens separately in applyQbit when the block is available.
type QbitEvent struct {
From ids.NodeID // The node that sent the Qbit
BlockID ids.ID // The block being signaled (preferredID)
RequestID uint32 // Request ID for dedup and stale detection
ReceivedAt time.Time // When the Qbit was received
}
// contextRequest tracks a pending context request (wire: GetAncestors)
type contextRequest struct {
nodeID ids.NodeID
requestID uint32
blockID ids.ID
timestamp time.Time
}
func newBlockHandler(vm chain.ChainVM, logger log.Logger, engine *consensuschain.Runtime, net network.Network, msgCreator message.OutboundMsgBuilder, chainID ids.ID, networkID ids.ID) *blockHandler {
return &blockHandler{
vm: vm,
logger: logger,
engine: engine,
net: net,
msgCreator: msgCreator,
chainID: chainID,
networkID: networkID,
pendingContext: make(map[ids.ID]contextRequest),
maxContextBlocks: 256, // Default max context blocks to request/serve
pendingQbits: make(map[ids.ID][]QbitEvent),
}
}
// bufferQbit stores a QbitEvent for later processing when the block isn't available yet
func (b *blockHandler) bufferQbit(ev QbitEvent) {
b.pendingQbitMu.Lock()
defer b.pendingQbitMu.Unlock()
// Add to buffer, limiting max buffered Qbits per block to prevent memory growth
const maxQbitsPerBlock = 100
existing := b.pendingQbits[ev.BlockID]
if len(existing) >= maxQbitsPerBlock {
return // Don't buffer more
}
b.pendingQbits[ev.BlockID] = append(existing, ev)
}
// popBufferedQbits removes and returns all buffered QbitEvents for a given block
func (b *blockHandler) popBufferedQbits(blockID ids.ID) []QbitEvent {
b.pendingQbitMu.Lock()
defer b.pendingQbitMu.Unlock()
evs := b.pendingQbits[blockID]
delete(b.pendingQbits, blockID)
return evs
}
// hasBlock returns true if the block is available (either in consensus pendingBlocks or VM storage).
// This is critical for Qbit handling: when we receive votes for a block we've built or received,
// the block may only be in pendingBlocks (not yet verified/stored in VM).
func (b *blockHandler) hasBlock(ctx context.Context, blockID ids.ID) bool {
// First check if the block is in consensus pending (built or received but not yet finalized).
// This allows votes to be processed for blocks we're currently considering in consensus.
if b.engine != nil && b.engine.HasPendingBlock(blockID) {
return true
}
// Fall back to checking VM storage for verified blocks
if b.vm == nil {
return false
}
_, err := b.vm.GetBlock(ctx, blockID)
return err == nil
}
// enqueueQbit immediately processes a QbitEvent when the block is available
func (b *blockHandler) enqueueQbit(ctx context.Context, ev QbitEvent) {
b.applyQbit(ctx, ev)
}
// applyQbit derives a Vote from a QbitEvent and sends it to the consensus engine.
// This is the ONLY place where Vote creation happens.
func (b *blockHandler) applyQbit(ctx context.Context, ev QbitEvent) {
if b.engine == nil || b.vm == nil {
b.logger.Warn("engine or vm is nil",
log.Stringer("from", ev.From),
log.Stringer("blockID", ev.BlockID))
return
}
// Skip stale Qbits (older than 30 seconds)
if time.Since(ev.ReceivedAt) > 30*time.Second {
b.logger.Debug("skipping stale Qbit",
log.Stringer("from", ev.From),
log.Stringer("blockID", ev.BlockID),
log.Duration("age", time.Since(ev.ReceivedAt)))
return
}
// First check if the block is in consensus pending (recently proposed but not yet finalized).
var blk chain.Block
var err error
if pendingBlk, ok := b.engine.GetPendingBlock(ev.BlockID); ok {
blk = pendingBlk
b.logger.Debug("found block in consensus pending",
log.Stringer("from", ev.From),
log.Stringer("blockID", ev.BlockID))
} else {
// Fall back to VM storage for already-verified blocks
blk, err = b.vm.GetBlock(ctx, ev.BlockID)
if err != nil {
b.logger.Warn("block not found in pending or VM",
log.Stringer("from", ev.From),
log.Stringer("blockID", ev.BlockID),
log.Err(err))
return
}
b.logger.Debug("found block in VM storage",
log.Stringer("from", ev.From),
log.Stringer("blockID", ev.BlockID))
}
// Derive Accept from verification
accept := (blk.Verify(ctx) == nil)
// Create Vote from QbitEvent + local verification
vote := consensuschain.Vote{
BlockID: ev.BlockID,
NodeID: ev.From,
Accept: accept,
SignedAt: ev.ReceivedAt,
}
queued := b.engine.ReceiveVote(vote)
b.logger.Debug("sent Vote to consensus engine",
log.Stringer("from", ev.From),
log.Stringer("blockID", ev.BlockID),
log.Bool("accept", accept),
log.Bool("queued", queued))
}
// onBlockArrived is called when a block becomes available locally.
// It drains all buffered QbitEvents for that block and applies them.
func (b *blockHandler) onBlockArrived(ctx context.Context, blockID ids.ID) {
evs := b.popBufferedQbits(blockID)
if len(evs) == 0 {
return
}
b.logger.Info("draining buffered Qbits for arrived block",
log.Stringer("blockID", blockID),
log.Int("count", len(evs)))
for _, ev := range evs {
b.enqueueQbit(ctx, ev)
}
}
// isMissingContextError returns true if the error indicates missing prerequisite blocks
func isMissingContextError(err error) bool {
if err == nil {
return false
}
errStr := err.Error()
return strings.Contains(errStr, "unknown ancestor") ||
strings.Contains(errStr, "missing parent") ||
strings.Contains(errStr, "parent not found") ||
strings.Contains(errStr, "unknown parent") ||
strings.Contains(errStr, "missing context") ||
strings.Contains(errStr, "not found") // parent block not in local state
}
// requestContext sends a context request (wire: GetAncestors) to fetch missing blocks from a peer
func (b *blockHandler) requestContext(ctx context.Context, nodeID ids.NodeID, blockID ids.ID) {
if b.net == nil || b.msgCreator == nil {
return
}
b.contextRequestMu.Lock()
// Check if we already have a pending request for this block
if _, exists := b.pendingContext[blockID]; exists {
b.contextRequestMu.Unlock()
return
}
// Generate a new request ID
b.requestIDCounter++
requestID := b.requestIDCounter
// Record the pending request
b.pendingContext[blockID] = contextRequest{
nodeID: nodeID,
requestID: requestID,
blockID: blockID,
timestamp: time.Now(),
}
b.contextRequestMu.Unlock()
// Create and send context request (wire: GetAncestors message)
msg, err := b.msgCreator.GetAncestors(
b.chainID,
requestID,
10*time.Second, // Deadline
blockID,
p2p.EngineType_ENGINE_TYPE_CHAIN, // Use chain consensus engine type
)
if err != nil {
b.logger.Error("failed to create context request message",
log.Stringer("blockID", blockID),
log.Err(err))
return
}
nodeSet := set.NewSet[ids.NodeID](1)
nodeSet.Add(nodeID)
sentTo := b.net.Send(msg, nodeSet, b.networkID, 0)
b.logger.Info("requested context for missing prerequisites",
log.Stringer("from", nodeID),
log.Stringer("blockID", blockID),
log.Uint32("requestID", requestID),
log.Int("sentTo", sentTo.Len()))
}
func (b *blockHandler) Runtime() *runtime.Runtime { return nil }
func (b *blockHandler) Start(ctx context.Context, startReqID uint32) {}
func (b *blockHandler) Push(ctx context.Context, msg handler.Message) {}
func (b *blockHandler) Len() int { return 0 }
func (b *blockHandler) Get(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, msg []byte) error {
return nil
}
// GetContext responds to a request for verification context (parent chain blocks)
// starting from containerID. We respond with up to maxAncestors blocks in
// chronological order (oldest first) so the requester can attach the missing context.
func (b *blockHandler) GetContext(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, containerID ids.ID) error {
if b.vm == nil || b.net == nil || b.msgCreator == nil {
return nil
}
b.logger.Debug("received context request",
log.Stringer("from", nodeID),
log.Stringer("containerID", containerID),
log.Uint32("requestID", requestID))
// Collect context blocks (walk parent chain)
var containers [][]byte
currentID := containerID
for i := 0; i < b.maxContextBlocks; i++ {
// First check pending blocks (for recently proposed but not yet accepted blocks)
// This is critical: when we propose a block and send PullQuery, other validators
// request context for the proposed block which may not be accepted yet.
var blk chain.Block
var found bool
if b.engine != nil {
blk, found = b.engine.GetPendingBlock(currentID)
}
if !found {
// Fall back to VM storage for accepted blocks
var err error
blk, err = b.vm.GetBlock(ctx, currentID)
if err != nil {
// Block not found in either location, stop walking
break
}
}
// Add block bytes to the response (prepend to get oldest first)
blockBytes := blk.Bytes()
containers = append([][]byte{blockBytes}, containers...)
// Get parent ID for next iteration
parentID := blk.Parent()
if parentID == ids.Empty {
// Reached genesis, stop
break
}
currentID = parentID
}
if len(containers) == 0 {
b.logger.Debug("no context found for request",
log.Stringer("from", nodeID),
log.Stringer("containerID", containerID))
return nil
}
// Create and send Context response (wire protocol uses Ancestors message type)
msg, err := b.msgCreator.Ancestors(b.chainID, requestID, containers)
if err != nil {
b.logger.Error("failed to create context response",
log.Stringer("containerID", containerID),
log.Err(err))
return nil
}
nodeSet := set.NewSet[ids.NodeID](1)
nodeSet.Add(nodeID)
sentTo := b.net.Send(msg, nodeSet, b.networkID, 0)
b.logger.Info("sent context response",
log.Stringer("to", nodeID),
log.Stringer("containerID", containerID),
log.Int("numBlocks", len(containers)),
log.Int("sentTo", sentTo.Len()))
return nil
}
// handleContext processes an incoming context response (wire: Ancestors message).
// This is called when we previously requested context for a block we couldn't verify.
// Each block in the context is processed via Put to add it to our state.
// After processing, we drain any buffered Qbits that were waiting for context.
func (b *blockHandler) handleContext(ctx context.Context, nodeID ids.NodeID, requestID uint32, data []byte) error {
if b.vm == nil || len(data) == 0 {
return nil
}
b.logger.Debug("received context response",
log.Stringer("from", nodeID),
log.Uint32("requestID", requestID),
log.Int("dataLen", len(data)))
// The data encodes multiple blocks as:
// [len][block][len][block]... where len is a 4-byte big-endian length.
processed := 0
remaining := data
for len(remaining) >= 4 {
blockLen := int(binary.BigEndian.Uint32(remaining[:4]))
remaining = remaining[4:]
if blockLen <= 0 || blockLen > len(remaining) {
b.logger.Debug("invalid context block length",
log.Stringer("from", nodeID),
log.Int("processed", processed),
log.Int("blockLen", blockLen),
log.Int("remaining", len(remaining)))
break
}
blockBytes := remaining[:blockLen]
remaining = remaining[blockLen:]
if err := b.Put(ctx, nodeID, requestID, blockBytes); err != nil {
b.logger.Debug("failed to process context block",
log.Stringer("from", nodeID),
log.Int("processed", processed),
log.Err(err))
break
}
processed++
}
b.logger.Info("processed context blocks",
log.Stringer("from", nodeID),
log.Int("processed", processed))
return nil
}
func (b *blockHandler) GetAcceptedFrontier(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time) error {
return nil
}
func (b *blockHandler) GetAccepted(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, containerIDs []ids.ID) error {
return nil
}
func (b *blockHandler) Put(ctx context.Context, nodeID ids.NodeID, requestID uint32, container []byte) error {
// Route incoming block through consensus engine instead of auto-accepting
// This enables proper quorum-based block acceptance
if b.engine == nil {
b.logger.Warn("no consensus engine - cannot process incoming block",
log.Stringer("from", nodeID))
return nil
}
// Use the consensus engine to handle the block properly:
// 1. Parse and verify the block
// 2. Add to pending blocks
// 3. Vote on the block
// 4. Accept only when quorum is reached
blk, err := b.engine.HandleIncomingBlock(ctx, container, nodeID)
if err != nil {
if isMissingContextError(err) {
if b.vm != nil {
missingBlk, parseErr := b.vm.ParseBlock(ctx, container)
if parseErr == nil {
b.requestContext(ctx, nodeID, missingBlk.ID())
} else {
b.logger.Debug("failed to parse missing-context block",
log.Stringer("from", nodeID),
log.Err(parseErr))
}
}
}
b.logger.Debug("failed to handle incoming block",
log.Stringer("from", nodeID),
log.Err(err))
return nil
}
if blk != nil {
blockID := blk.ID()
b.logger.Info("processed incoming block through consensus",
log.Stringer("from", nodeID),
log.Stringer("blockID", blockID),
log.Uint64("height", blk.Height()))
// Drain any buffered Qbits that were waiting for this block
b.onBlockArrived(ctx, blockID)
}
return nil
}
func (b *blockHandler) PushQuery(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, container []byte) error {
// PushQuery sends block data AND expects a Qbit response.
// CRITICAL: We must ALWAYS respond with Chits, even if we can't fully
// process the block. This is how Quasar consensus works - the proposer
// needs vote responses to reach quorum.
b.logger.Debug("received PushQuery",
log.Stringer("from", nodeID),
log.Uint32("requestID", requestID),
log.Int("containerLen", len(container)))
if b.net == nil || b.msgCreator == nil || b.vm == nil {
b.logger.Warn("missing net/msgCreator/vm - cannot respond")
return nil
}
// 1. Process the block via Put (may trigger context fetch asynchronously)
if err := b.Put(ctx, nodeID, requestID, container); err != nil {
b.logger.Debug("Put returned error (will still respond with Chits)",
log.Stringer("from", nodeID),
log.Err(err))
}
// 2. Get the correct block ID by parsing the block through the VM.
// CRITICAL: The block ID must match the proposer's vmBlock.ID() exactly.
// The VM computes block IDs using its own hash function (e.g., Keccak256 for EVM).
// Using SHA256(containerBytes) would produce a DIFFERENT ID, causing votes to
// never match the proposer's pending block, and consensus would never accept.
var preferredID ids.ID
if b.vm != nil {
if blk, err := b.vm.ParseBlock(ctx, container); err == nil {
preferredID = blk.ID()
} else {
b.logger.Warn("ParseBlock failed, cannot vote correctly",
log.Stringer("from", nodeID),
log.Err(err))
// Cannot vote correctly without the proper block ID - respond with last accepted
acceptedBlkID, _ := b.vm.LastAccepted(ctx)
preferredID = acceptedBlkID
}
}
b.logger.Debug("using VM block ID for vote",
log.Stringer("from", nodeID),
log.Stringer("blockID", preferredID))
// 3. Get the last accepted block info for the Chits response
acceptedBlkID, err := b.vm.LastAccepted(ctx)
if err != nil {
b.logger.Warn("failed to get last accepted - responding with empty accepted",
log.Stringer("from", nodeID),
log.Err(err))
// Use empty ID as fallback
acceptedBlkID = ids.Empty
}
var acceptedHeight uint64
if acceptedBlkID != ids.Empty {
acceptedBlk, err := b.vm.GetBlock(ctx, acceptedBlkID)
if err != nil {
b.logger.Debug("failed to get accepted block (using height 0)",
log.Stringer("from", nodeID),
log.Stringer("acceptedBlkID", acceptedBlkID),
log.Err(err))
// Use height 0 as fallback - this is OK for genesis state
} else {
acceptedHeight = acceptedBlk.Height()
}
}
// 4. Create Chits response (vote for the proposed block)
qbitMsg, err := b.msgCreator.Chits(b.chainID, requestID, preferredID, preferredID, acceptedBlkID, acceptedHeight)
if err != nil {
b.logger.Error("failed to create Chits message",
log.Stringer("from", nodeID),
log.Stringer("blockID", preferredID),
log.Err(err))
return nil
}
// 5. Send Chits response to the requesting node
nodeSet := set.NewSet[ids.NodeID](1)
nodeSet.Add(nodeID)
sentTo := b.net.Send(qbitMsg, nodeSet, b.networkID, 0)
b.logger.Debug("responded with Chits",
log.Stringer("from", nodeID),
log.Stringer("preferredID", preferredID),
log.Stringer("acceptedID", acceptedBlkID),
log.Uint64("acceptedHeight", acceptedHeight),
log.Int("sentTo", sentTo.Len()))
return nil
}
func (b *blockHandler) PullQuery(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, containerID ids.ID) error {
// PullQuery requests a preference signal on a block identified by containerID
// We respond with a Qbit (wire: p2p.Chits) containing our preference
if b.net == nil || b.msgCreator == nil {
b.logger.Debug("cannot respond to PullQuery - no network sender",
log.Stringer("from", nodeID),
log.Stringer("blockID", containerID))
return nil
}
// Try to get the block from pending blocks first, then VM storage.
// When we receive a PullQuery, the proposer has the block but it may not
// be in our VM storage yet. First check pending blocks (proposed but not accepted).
var blk chain.Block
var found bool
// First check pending blocks (for recently proposed but not yet accepted blocks)
if b.engine != nil {
blk, found = b.engine.GetPendingBlock(containerID)
if found {
b.logger.Debug("found block in pending for PullQuery",
log.Stringer("blockID", containerID),
log.Uint64("height", blk.Height()))
}
}
// If not in pending, try VM storage with retries
if !found {
var err error
const maxRetries = 3
const retryDelay = 20 * time.Millisecond
for attempt := 0; attempt < maxRetries; attempt++ {
blk, err = b.vm.GetBlock(ctx, containerID)
if err == nil {
found = true
break // Found the block
}
if attempt < maxRetries-1 {
// Wait before retry to allow concurrent Put to complete
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(retryDelay):
// Continue to next attempt
}
}
}
}
if !found {
b.logger.Debug("cannot respond to PullQuery - block not found in pending or storage",
log.Stringer("from", nodeID),
log.Stringer("blockID", containerID))
// Block not found - request context (parent chain) from peer
b.requestContext(ctx, nodeID, containerID)
return nil
}
// Verify the block before voting for it
if err := blk.Verify(ctx); err != nil {
b.logger.Debug("cannot respond to PullQuery - block verification failed",
log.Stringer("from", nodeID),
log.Stringer("blockID", containerID),
log.Err(err))
// If verification failed due to missing context, request it from peer
if isMissingContextError(err) {
b.logger.Info("block missing context - requesting from peer",
log.Stringer("from", nodeID),
log.Stringer("blockID", containerID))
b.requestContext(ctx, nodeID, containerID)
}
return nil
}
// Get the accepted block ID (last accepted) for the acceptedID field
acceptedBlkID := containerID
acceptedHeight := blk.Height()
// Try to get the last accepted block for more accurate acceptedID
if lastAccepted, err := b.vm.LastAccepted(ctx); err == nil && lastAccepted != ids.Empty {
acceptedBlkID = lastAccepted
if acceptedBlk, err := b.vm.GetBlock(ctx, lastAccepted); err == nil {
acceptedHeight = acceptedBlk.Height()
}
}
// Create Qbit response message (wire: p2p.Chits)
// preferredID: the block we prefer
// preferredIDAtHeight: same as preferredID
// acceptedID: the last accepted block
// acceptedHeight: height of the accepted block
qbitMsg, err := b.msgCreator.Chits(b.chainID, requestID, containerID, containerID, acceptedBlkID, acceptedHeight)
if err != nil {
b.logger.Error("failed to create Qbit message",
log.Stringer("from", nodeID),
log.Stringer("blockID", containerID),
log.Err(err))
return nil
}
// Send Qbit response to the requesting node
nodeSet := set.NewSet[ids.NodeID](1)
nodeSet.Add(nodeID)
sentTo := b.net.Send(qbitMsg, nodeSet, b.networkID, 0)
b.logger.Debug("responded to PullQuery with Qbit",
log.Stringer("from", nodeID),
log.Stringer("blockID", containerID),
log.Uint64("height", blk.Height()),
log.Int("sentTo", sentTo.Len()))
return nil
}
func (b *blockHandler) QueryFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32) error {
return nil
}
func (b *blockHandler) CrossChainRequest(ctx context.Context, chainID ids.ID, requestID uint32, deadline time.Time, msg []byte) error {
return nil
}
func (b *blockHandler) CrossChainRequestFailed(ctx context.Context, chainID ids.ID, requestID uint32) error {
return nil
}
func (b *blockHandler) CrossChainResponse(ctx context.Context, chainID ids.ID, requestID uint32, msg []byte) error {
return nil
}
func (b *blockHandler) Request(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, msg []byte) error {
return nil
}
func (b *blockHandler) RequestFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32) error {
return nil
}
func (b *blockHandler) Response(ctx context.Context, nodeID ids.NodeID, requestID uint32, msg []byte) error {
return nil
}
func (b *blockHandler) Gossip(ctx context.Context, nodeID ids.NodeID, msg []byte) error {
// Handle Gossip - try to process as block
return b.Put(ctx, nodeID, 0, msg)
}
func (b *blockHandler) GetStateSummaryFrontier(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time) error {
return nil
}
func (b *blockHandler) StateSummaryFrontier(ctx context.Context, nodeID ids.NodeID, requestID uint32, summary []byte) error {
return nil
}
func (b *blockHandler) GetAcceptedStateSummary(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, heights []uint64) error {
return nil
}
func (b *blockHandler) AcceptedStateSummary(ctx context.Context, nodeID ids.NodeID, requestID uint32, summaryIDs []ids.ID) error {
return nil
}
func (b *blockHandler) GetStateSummary(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, height uint64) error {
return nil
}
func (b *blockHandler) StateSummary(ctx context.Context, nodeID ids.NodeID, requestID uint32, summary []byte) error {
return nil
}
func (b *blockHandler) Connected(ctx context.Context, nodeID ids.NodeID) error { return nil }
func (b *blockHandler) Disconnected(ctx context.Context, nodeID ids.NodeID) error { return nil }
func (b *blockHandler) HealthCheck(ctx context.Context) (interface{}, error) { return nil, nil }
func (b *blockHandler) Stop(ctx context.Context) {}
func (b *blockHandler) HandleInbound(ctx context.Context, msg handler.Message) error {
// Dispatch based on Op type
switch msg.Op {
case handler.PushQuery:
// PushQuery contains block data AND expects a Qbit (vote) response
if len(msg.Message) > 0 {
return b.PushQuery(ctx, msg.NodeID, msg.RequestID, time.Now().Add(10*time.Second), msg.Message)
}
case handler.Put:
// Put contains block data but does not expect a vote response
if len(msg.Message) > 0 {
return b.Put(ctx, msg.NodeID, msg.RequestID, msg.Message)
}
case handler.PullQuery:
// PullQuery asks for a preference signal on a block identified by ID
// Extract the blockID from the message and respond with Qbit
if len(msg.Message) >= 32 {
var containerID ids.ID
copy(containerID[:], msg.Message[:32])
return b.PullQuery(ctx, msg.NodeID, msg.RequestID, time.Now().Add(10*time.Second), containerID)
}
case handler.Vote:
// Vote contains a preference signal for a block (preferredID)
// Note: msg.Message already contains the extracted PreferredId from the Qbit protobuf
// (extracted by chain_router.go via GetContainerBytes which returns m.GetPreferredId())
b.logger.Debug("received Chits message",
log.Stringer("from", msg.NodeID),
log.Uint32("requestID", msg.RequestID),
log.Int("msgLen", len(msg.Message)))
if len(msg.Message) >= 32 {
var preferredID ids.ID
copy(preferredID[:], msg.Message[:32])
b.logger.Debug("extracted preferredID",
log.Stringer("from", msg.NodeID),
log.Stringer("preferredID", preferredID))
// Create QbitEvent - pure data, no VM calls here
ev := QbitEvent{
From: msg.NodeID,
BlockID: preferredID,
RequestID: msg.RequestID,
ReceivedAt: time.Now(),
}
// If block is missing, buffer the event and return
if !b.hasBlock(ctx, preferredID) {
b.bufferQbit(ev)
b.logger.Debug("buffered Qbit - block not in pending or VM",
log.Stringer("from", ev.From),
log.Stringer("blockID", ev.BlockID))
return nil
}
b.logger.Debug("block found - enqueuing for processing",
log.Stringer("from", ev.From),
log.Stringer("blockID", ev.BlockID))
// Block is available - enqueue for processing
// Vote creation happens in applyQbit, not here
b.enqueueQbit(ctx, ev)
} else {
b.logger.Warn("message too short for preferredID",
log.Stringer("from", msg.NodeID),
log.Int("msgLen", len(msg.Message)))
}
case handler.GetContext:
// GetContext requests verification context (parent chain) for a block
if len(msg.Message) >= 32 {
var containerID ids.ID
copy(containerID[:], msg.Message[:32])
return b.GetContext(ctx, msg.NodeID, msg.RequestID, time.Now().Add(10*time.Second), containerID)
}
case handler.Context:
// Context contains prerequisite blocks - process each one via Put
return b.handleContext(ctx, msg.NodeID, msg.RequestID, msg.Message)
}
return nil
}
func (b *blockHandler) HandleOutbound(ctx context.Context, msg handler.Message) error {
return nil
}
// noopHandler implements handler.Handler interface
type noopHandler struct{}
func (p *noopHandler) Runtime() *runtime.Runtime { return nil }
func (p *noopHandler) Start(ctx context.Context, startReqID uint32) {}
func (p *noopHandler) Push(ctx context.Context, msg handler.Message) {}
func (p *noopHandler) Len() int { return 0 }
func (p *noopHandler) Get(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, msg []byte) error {
return nil
}
func (p *noopHandler) GetContext(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, containerID ids.ID) error {
return nil
}
func (p *noopHandler) GetAcceptedFrontier(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time) error {
return nil
}
func (p *noopHandler) GetAccepted(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, containerIDs []ids.ID) error {
return nil
}
func (p *noopHandler) Put(ctx context.Context, nodeID ids.NodeID, requestID uint32, container []byte) error {
return nil
}
func (p *noopHandler) PushQuery(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, container []byte) error {
return nil
}
func (p *noopHandler) PullQuery(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, containerID ids.ID) error {
return nil
}
func (p *noopHandler) QueryFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32) error {
return nil
}
func (p *noopHandler) CrossChainRequest(ctx context.Context, chainID ids.ID, requestID uint32, deadline time.Time, msg []byte) error {
return nil
}
func (p *noopHandler) CrossChainRequestFailed(ctx context.Context, chainID ids.ID, requestID uint32) error {
return nil
}
func (p *noopHandler) CrossChainResponse(ctx context.Context, chainID ids.ID, requestID uint32, msg []byte) error {
return nil
}
func (p *noopHandler) Request(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, msg []byte) error {
return nil
}
func (p *noopHandler) RequestFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32) error {
return nil
}
func (p *noopHandler) Response(ctx context.Context, nodeID ids.NodeID, requestID uint32, msg []byte) error {
return nil
}
func (p *noopHandler) Gossip(ctx context.Context, nodeID ids.NodeID, msg []byte) error {
return nil
}
func (p *noopHandler) GetStateSummaryFrontier(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time) error {
return nil
}
func (p *noopHandler) StateSummaryFrontier(ctx context.Context, nodeID ids.NodeID, requestID uint32, summary []byte) error {
return nil
}
func (p *noopHandler) GetAcceptedStateSummary(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, heights []uint64) error {
return nil
}
func (p *noopHandler) AcceptedStateSummary(ctx context.Context, nodeID ids.NodeID, requestID uint32, summaryIDs []ids.ID) error {
return nil
}
func (p *noopHandler) GetStateSummary(ctx context.Context, nodeID ids.NodeID, requestID uint32, deadline time.Time, height uint64) error {
return nil
}
func (p *noopHandler) StateSummary(ctx context.Context, nodeID ids.NodeID, requestID uint32, summary []byte) error {
return nil
}
func (p *noopHandler) Connected(ctx context.Context, nodeID ids.NodeID) error { return nil }
func (p *noopHandler) Disconnected(ctx context.Context, nodeID ids.NodeID) error { return nil }
func (p *noopHandler) HealthCheck(ctx context.Context) (interface{}, error) { return nil, nil }
func (p *noopHandler) Stop(ctx context.Context) {}
func (p *noopHandler) HandleInbound(ctx context.Context, msg handler.Message) error {
return nil
}
func (p *noopHandler) HandleOutbound(ctx context.Context, msg handler.Message) error {
return nil
}
// noopWarpSender is a no-op implementation of warp.Sender for cross-chain messaging
// Used in single-node mode where cross-chain messaging is not needed
type noopWarpSender struct{}
// Compile-time check that noopWarpSender implements warp.Sender
var _ warp.Sender = (*noopWarpSender)(nil)
func (n *noopWarpSender) SendRequest(ctx context.Context, nodeIDs set.Set[ids.NodeID], requestID uint32, request []byte) error {
return nil
}
func (n *noopWarpSender) SendResponse(ctx context.Context, nodeID ids.NodeID, requestID uint32, response []byte) error {
return nil
}
func (n *noopWarpSender) SendError(ctx context.Context, nodeID ids.NodeID, requestID uint32, errorCode int32, errorMessage string) error {
return nil
}
func (n *noopWarpSender) SendGossip(ctx context.Context, config warp.SendConfig, gossipBytes []byte) error {
return nil
}
// networkGossiper implements consensuschain.Gossiper for Lux consensus integration.
// It adapts the node's network layer to the minimal Gossiper interface used by
// the integrated consensus engine.
type networkGossiper struct {
net network.Network
msgCreator message.OutboundMsgBuilder
networkID ids.ID
}
// Compile-time check that networkGossiper implements Gossiper
var _ consensuschain.Gossiper = (*networkGossiper)(nil)
// GossipPut broadcasts a Put message with block data to validators.
func (g *networkGossiper) GossipPut(chainID ids.ID, networkID ids.ID, blockData []byte) int {
if g.net == nil || g.msgCreator == nil {
return 0
}
putMsg, err := g.msgCreator.Put(chainID, 0, blockData)
if err != nil {
return 0
}
// Gossip to all validators (-1 = all validators)
// Must use g.networkID (chain ID) not chainID (blockchain ID) —
// net.Gossip filters validators by chain ID in the validator manager.
sentTo := g.net.Gossip(putMsg, nil, g.networkID, -1, 0, 0)
return sentTo.Len()
}
// SendPullQuery sends a PullQuery (blockID only) to validators requesting votes.
// Prefer SendPushQuery when block bytes are available for immediate peer verification.
func (g *networkGossiper) SendPullQuery(chainID ids.ID, networkID ids.ID, blockID ids.ID, validators []ids.NodeID) int {
if g.net == nil || g.msgCreator == nil {
return 0
}
pullMsg, err := g.msgCreator.PullQuery(chainID, 0, 5*time.Second, blockID, 0)
if err != nil {
return 0
}
if len(validators) == 0 {
return g.net.Gossip(pullMsg, nil, chainID, -1, 0, 0).Len()
}
validatorSet := set.NewSet[ids.NodeID](len(validators))
for _, v := range validators {
validatorSet.Add(v)
}
return g.net.Send(pullMsg, validatorSet, chainID, 0).Len()
}
// SendPushQuery sends a PushQuery (block bytes + vote request) to validators.
// PushQuery includes the block data so peers can immediately verify and vote,
// unlike PullQuery which only sends the blockID requiring a separate fetch.
func (g *networkGossiper) SendPushQuery(chainID ids.ID, networkID ids.ID, blockData []byte, validators []ids.NodeID) int {
if g.net == nil || g.msgCreator == nil {
return 0
}
pushMsg, err := g.msgCreator.PushQuery(chainID, 0, 5*time.Second, blockData, 0)
if err != nil {
log.Warn("SendPushQuery: message creation failed",
"chainID", chainID,
"error", err,
)
return 0
}
log.Info("SendPushQuery: sending to network",
"chainID", chainID,
"networkID", networkID,
"blockDataLen", len(blockData),
"numValidators", len(validators),
)
if len(validators) == 0 {
return g.net.Gossip(pushMsg, nil, chainID, -1, 0, 0).Len()
}
validatorSet := set.NewSet[ids.NodeID](len(validators))
for _, v := range validators {
validatorSet.Add(v)
}
return g.net.Send(pushMsg, validatorSet, chainID, 0).Len()
}
// SendQbit sends a preference response (Qbit) back to the node that requested our preference.
// This is called after verifying a block received via PullQuery.
func (g *networkGossiper) SendQbit(toNodeID ids.NodeID, chainID ids.ID, requestID uint32, preferredID ids.ID) error {
if g.net == nil || g.msgCreator == nil {
return nil
}
// Create Qbit message (wire: p2p.Chits) with the preferred block ID
// Uses preferredID as both preferred and accepted (block is verified)
qbitMsg, err := g.msgCreator.Chits(chainID, requestID, preferredID, preferredID, preferredID, 0)
if err != nil {
return err
}
// Send to the specific node
nodeSet := set.Of(toNodeID)
g.net.Send(qbitMsg, nodeSet, g.networkID, 0)
return nil
}
// SendVote sends a vote response back to the proposer node after fast-follow acceptance.
// This is required by the consensuschain.Gossiper interface.
func (g *networkGossiper) SendVote(chainID ids.ID, toNodeID ids.NodeID, blockID ids.ID) error {
if g.net == nil || g.msgCreator == nil {
return nil
}
// Create a Chits message to send the vote
// Use blockID as all three IDs (preferred, accepted, last accepted)
// since this is a positive vote confirming we've accepted the block
voteMsg, err := g.msgCreator.Chits(chainID, 0, blockID, blockID, blockID, 0)
if err != nil {
return err
}
// Send to the proposer node
nodeSet := set.Of(toNodeID)
g.net.Send(voteMsg, nodeSet, g.networkID, 0)
return nil
}