Files
zeekayandHanzo Dev c3d6105804 feat(proposervm): configurable MinBlkDelay + timestamp granularity tracks WindowDuration (sub-second cadence foundation)
Two coupled knobs for differentiated cadence — co-located D-Chain/DEX (fast) vs public
C-Chain (standard):

1. MinBlkDelay was hardcoded to the 1s DefaultMinBlockDelay in chains/manager.go, ignoring
   its own --proposervm-min-block-delay flag. Now wired node.Config → ManagerConfig →
   proposervm.Config (0 ⇒ 1s default). High-throughput nets set it low.

2. Block timestamps were Truncate(time.Second) at 4 sites, quantizing cadence to 1s
   regardless of WindowDuration — so a sub-second window inflated slot numbers without finer
   time resolution and could NOT produce blocks faster than 1/s. Truncation now tracks
   proposer.TimestampGranularity() = min(1s, WindowDuration): mainnet (>=1s) keeps exact
   1-second block times; sub-second windows get matching sub-second timestamps.

Measured: 1s window/delay → ~95-104 TPS, byte-identical 5/5 finality (unchanged from before,
no regression). NOTE: true sub-second cadence additionally requires a slot-handoff fix — at
100ms the slot recomputed in buildChild from a drifted 'now' can differ from the slot
timeToBuild scheduled, causing errUnexpectedProposer verify drops; that + multi-sender
saturation is the next step. These knobs are the necessary foundation.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-15 22:33:01 -07:00

2220 lines
73 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package node
import (
"bytes"
"context"
"crypto"
"crypto/tls"
"errors"
"fmt"
"io"
"io/fs"
"net"
"net/netip"
"os"
"path/filepath"
"strconv"
"sync"
"time"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
nodevalidators "github.com/luxfi/validators"
"github.com/luxfi/metric"
consensusconfig "github.com/luxfi/consensus/config"
"github.com/luxfi/consensus/networking/timeout"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/database"
"github.com/luxfi/database/prefixdb"
genesiscfg "github.com/luxfi/genesis/pkg/genesis"
genesissecurity "github.com/luxfi/genesis/pkg/genesis/security"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
"github.com/luxfi/node/benchlist"
"github.com/luxfi/node/chains"
"github.com/luxfi/node/config/node"
nodeconsensus "github.com/luxfi/node/consensus"
"github.com/luxfi/node/genesis/builder"
"github.com/luxfi/node/indexer"
"github.com/luxfi/node/message"
"github.com/luxfi/node/nat"
"github.com/luxfi/node/network"
"github.com/luxfi/node/network/dialer"
"github.com/luxfi/node/network/peer"
"github.com/luxfi/node/network/throttling"
"github.com/luxfi/node/network/tracker"
"github.com/luxfi/node/server/http"
"github.com/luxfi/node/service/admin"
"github.com/luxfi/node/service/health"
"github.com/luxfi/node/service/info"
"github.com/luxfi/node/service/keystore"
"github.com/luxfi/node/service/metrics"
luxsecurity "github.com/luxfi/node/service/security"
"github.com/luxfi/node/staking"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/node/version"
"github.com/luxfi/node/vms"
"github.com/luxfi/node/vms/platformvm"
platformvmgenesis "github.com/luxfi/node/vms/platformvm/genesis"
"github.com/luxfi/node/vms/rpcchainvm/runtime"
"github.com/luxfi/node/vms/txs/auth"
"github.com/luxfi/node/vms/xvm"
"github.com/luxfi/validators/uptime"
"github.com/luxfi/vm/chains/atomic"
hash "github.com/luxfi/crypto/hash"
"github.com/luxfi/filesystem"
"github.com/luxfi/filesystem/perms"
"github.com/luxfi/net/dynamicip"
"github.com/luxfi/net/endpoints"
"github.com/luxfi/node/trace"
"github.com/luxfi/node/utils/profiler"
"github.com/luxfi/node/vms/platformvm/signer"
"github.com/luxfi/node/vms/registry"
"github.com/luxfi/resource"
"github.com/luxfi/utils"
lux "github.com/luxfi/utxo"
databasefactory "github.com/luxfi/database/factory"
platformconfig "github.com/luxfi/node/vms/platformvm/config"
gpuconfig "github.com/luxfi/node/config"
)
const (
stakingPortName = constants.AppName + "-staking"
httpPortName = constants.AppName + "-http"
ipResolutionTimeout = 30 * time.Second
apiNamespace = constants.PlatformName + "_" + "api"
benchlistNamespace = constants.PlatformName + "_" + "benchlist"
dbNamespace = constants.PlatformName + "_" + "db"
healthNamespace = constants.PlatformName + "_" + "health"
meterDBNamespace = constants.PlatformName + "_" + "meterdb"
networkNamespace = constants.PlatformName + "_" + "network"
processNamespace = constants.PlatformName + "_" + "process"
requestsNamespace = constants.PlatformName + "_" + "requests"
resourceTrackerNamespace = constants.PlatformName + "_" + "resource_tracker"
responsesNamespace = constants.PlatformName + "_" + "responses"
rpcchainvmNamespace = constants.PlatformName + "_" + "rpcchainvm"
systemResourcesNamespace = constants.PlatformName + "_" + "system_resources"
)
var (
genesisHashKey = []byte("genesisID")
ungracefulShutdown = []byte("ungracefulShutdown")
indexerDBPrefix = []byte{0x00}
errInvalidTLSKey = errors.New("invalid TLS key")
errShuttingDown = errors.New("server shutting down")
)
// New returns an instance of Node
func New(
config *node.Config,
logFactory log.Factory,
logger log.Logger,
) (*Node, error) {
tlsCert := config.StakingTLSCert.Leaf
stakingCert, err := staking.ParseCertificate(tlsCert.Raw)
if err != nil {
return nil, fmt.Errorf("invalid staking certificate: %w", err)
}
// NodeID derivation routes through the single seam
// StakingConfig.DeriveNodeID — strict-PQ chains (StakingMLDSAPub
// non-empty) derive NodeID via SHAKE256-384("NODE_ID_V1" ||
// chainID || 0x42 || pubKey)[:20] under ids.NodeIDSchemeMLDSA65;
// classical-compat chains fall through to ids.NodeIDFromCert.
// chainID is the primary-network chain id (ids.Empty, encoded as
// "11111111111111111111111111111111LpoYY" in cb58); per-chain
// chain ids are bound at chain-creation time and don't affect
// the validator's primary identity.
derivedNodeID, err := config.StakingConfig.DeriveNodeID(ids.Empty)
if err != nil {
return nil, fmt.Errorf("derive NodeID: %w", err)
}
n := &Node{
Log: logger,
LogFactory: logFactory,
StakingTLSCert: stakingCert,
ID: derivedNodeID,
Config: config,
}
pop, err := signer.NewProofOfPossession(n.Config.StakingSigningKey)
if err != nil {
return nil, fmt.Errorf("problem creating proof of possession: %w", err)
}
logger.Info("initializing node",
log.Stringer("version", version.CurrentApp),
log.String("commit", version.GitCommit),
log.Stringer("nodeID", n.ID),
log.Stringer("stakingKeyType", tlsCert.PublicKeyAlgorithm),
log.Reflect("nodePOP", pop),
log.Reflect("providedFlags", n.Config.ProvidedFlags),
log.Reflect("config", n.Config),
)
// Log GPU acceleration status
gpuCfg := gpuconfig.GetGlobalGPUConfig()
gpuBackend := gpuCfg.ResolveBackend()
gpuAvailable := cgoEnabled && gpuCfg.Enabled && gpuBackend != "cpu"
logger.Info("GPU acceleration status",
log.Bool("available", gpuAvailable),
log.String("backend", gpuBackend),
log.Bool("enabled", gpuCfg.Enabled),
log.Bool("cgoEnabled", cgoEnabled),
log.Int("deviceIndex", gpuCfg.DeviceIndex),
)
n.VMFactoryLog = n.Log // Use main log instead of vm-factory specific log
n.VMAliaser = ids.NewAliaser()
for vmID, aliases := range config.VMAliases {
for _, alias := range aliases {
if err := n.VMAliaser.Alias(vmID, alias); err != nil {
return nil, err
}
}
}
n.VMManager = vms.NewManager()
if err := n.initBootstrappers(); err != nil { // Configure the bootstrappers
return nil, fmt.Errorf("problem initializing node beacons: %w", err)
}
// Load the chain-wide ChainSecurityProfile from the genesis pin
// BEFORE any networking / chain manager / VM init runs — those
// consumers should see the resolved profile (or its absence) via
// n.SecurityProfile() at construction time. Closes F102.
if err := n.initSecurityProfile(); err != nil {
return nil, fmt.Errorf("problem loading security profile: %w", err)
}
// Set up tracer
n.tracer, err = trace.New(n.Config.TraceConfig)
if err != nil {
return nil, fmt.Errorf("couldn't initialize tracer: %w", err)
}
if err := n.initMetrics(); err != nil {
return nil, fmt.Errorf("couldn't initialize metrics: %w", err)
}
n.initNAT()
if err := n.initAPIServer(); err != nil { // Start the API Server
return nil, fmt.Errorf("couldn't initialize API server: %w", err)
}
if err := n.initMetricsAPI(); err != nil { // Start the Metrics API
return nil, fmt.Errorf("couldn't initialize metrics API: %w", err)
}
if err := n.initDatabase(); err != nil { // Set up the node's database
return nil, fmt.Errorf("problem initializing database: %w", err)
}
// Start streaming replication if REPLICATE_S3_ENDPOINT is set.
// Unwraps through meterdb/versiondb to reach the underlying zapdb.
if rep, ok := database.UnwrapTo[database.Replicatable](n.DB); ok {
if err := rep.StartReplicator(context.Background()); err != nil {
n.Log.Warn("failed to start database replication", "error", err)
}
}
n.initSharedMemory() // Initialize shared memory
// message.Creator is shared between networking, chainManager and the engine.
// It must be initiated before networking (initNetworking), chain manager (initChainManager)
// and the engine (initChains) but after the metrics (initMetricsAPI)
// message.Creator currently record metrics under network namespace
networkRegisterer, err := metric.MakeAndRegister(
n.MetricsGatherer,
networkNamespace,
)
if err != nil {
return nil, err
}
n.msgCreator, err = message.NewCreator(
networkRegisterer,
n.Config.NetworkConfig.CompressionType,
n.Config.NetworkConfig.MaximumInboundMessageTimeout,
)
if err != nil {
return nil, fmt.Errorf("problem initializing message creator: %w", err)
}
// Create a simple validator manager implementation
// Since validators.NewManager doesn't exist, we use our platformvm implementation
n.vdrs = nodevalidators.NewManager()
if !n.Config.SybilProtectionEnabled {
logger.Warn("sybil control is not enforced")
n.vdrs = newOverriddenManager(constants.PrimaryNetworkID, n.vdrs)
}
// NOTE: Validators are populated by PlatformVM during its initialization.
// The network layer will use n.vdrs which gets populated when PlatformVM
// loads validators from genesis. Do NOT pre-populate n.vdrs here as it
// breaks PlatformVM's assumption that the validator set starts empty.
if err := n.initResourceManager(); err != nil {
return nil, fmt.Errorf("problem initializing resource manager: %w", err)
}
n.initCPUTargeter(&config.CPUTargeterConfig)
n.initDiskTargeter(&config.DiskTargeterConfig)
if err := n.initNetworking(networkRegisterer); err != nil { // Set up networking layer.
return nil, fmt.Errorf("problem initializing networking: %w", err)
}
n.initEventDispatchers()
// Start the Health API
// Has to be initialized before chain manager
// [n.Net] must already be set
if err := n.initHealthAPI(); err != nil {
return nil, fmt.Errorf("couldn't initialize health API: %w", err)
}
if err := n.addDefaultVMAliases(); err != nil {
return nil, fmt.Errorf("couldn't initialize API aliases: %w", err)
}
if err := n.initChainManager(n.Config.UTXOAssetID); err != nil { // Set up the chain manager
return nil, fmt.Errorf("couldn't initialize chain manager: %w", err)
}
if err := n.initVMs(); err != nil { // Initialize the VM registry.
return nil, fmt.Errorf("couldn't initialize VM registry: %w", err)
}
if err := n.initAdminAPI(); err != nil { // Start the Admin API
return nil, fmt.Errorf("couldn't initialize admin API: %w", err)
}
if err := n.initInfoAPI(); err != nil { // Start the Info API
return nil, fmt.Errorf("couldn't initialize info API: %w", err)
}
if err := n.initSecurityAPI(); err != nil { // Start the securityProfile API
return nil, fmt.Errorf("couldn't initialize security API: %w", err)
}
if err := n.initKeystoreAPI(); err != nil { // Start the Keystore API
return nil, fmt.Errorf("couldn't initialize keystore API: %w", err)
}
if err := n.initChainAliases(n.Config.GenesisBytes); err != nil {
return nil, fmt.Errorf("couldn't initialize chain aliases: %w", err)
}
if err := n.initAPIAliases(n.Config.GenesisBytes); err != nil {
return nil, fmt.Errorf("couldn't initialize API aliases: %w", err)
}
if err := n.initIndexer(); err != nil {
return nil, fmt.Errorf("couldn't initialize indexer: %w", err)
}
healthCtx, healthCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer healthCancel()
n.health.Start(healthCtx, n.Config.HealthCheckFreq)
n.initProfiler()
// Start the Platform chain
if err := n.initChains(n.Config.GenesisBytes); err != nil {
return nil, fmt.Errorf("couldn't initialize chains: %w", err)
}
return n, nil
}
// Node is an instance of a Lux node.
type Node struct {
Log log.Logger
VMFactoryLog log.Logger
LogFactory log.Factory
// This node's unique ID used when communicating with other nodes
// (in consensus, for example)
ID ids.NodeID
StakingTLSSigner crypto.Signer
StakingTLSCert *staking.Certificate
// securityProfile is the chain-wide ChainSecurityProfile this node
// is operating under, loaded from the genesis SecurityProfile pin
// at boot via initSecurityProfile. nil when the genesis file
// carries no pin (legacy networks); non-nil after a successful
// pin resolution. Downstream consumers (signer registration, peer
// handshake, mempool, validator registry, bridge oracle) take this
// as a dependency via SecurityProfile().
//
// Closes red-team finding F102.
securityProfile *consensusconfig.ChainSecurityProfile
// Storage for this node
DB database.Database
router nat.Router
portMapper *nat.Mapper
ipUpdater dynamicip.Updater
chainRouter Router
// Profiles the process. Nil if continuous profiling is disabled.
profiler profiler.ContinuousProfiler
// Indexes blocks, transactions and blocks
indexer indexer.Indexer
// Manages shared memory
sharedMemory *atomic.Memory
// Monitors node health and runs health checks
health health.Health
// Manages user keystores
keystore keystore.Keystore
// Build and parse messages, for both network layer and chain manager
msgCreator message.Creator
// Manages network timeouts
timeoutManager timeout.Manager
// Manages creation of blockchains and routing messages to them
chainManager chains.Manager
// Manages validator benching
benchlistManager benchlist.Manager
uptimeCalculator uptime.LockedCalculator
// dispatcher for events as they happen in consensus
BlockAcceptorGroup nodeconsensus.AcceptorGroup
TxAcceptorGroup nodeconsensus.AcceptorGroup
VertexAcceptorGroup nodeconsensus.AcceptorGroup
// Net runs the networking stack
Net network.Network
// The staking address will optionally be written to a process context
// file to enable other nodes to be configured to use this node as a
// beacon.
stakingAddress netip.AddrPort
// tlsKeyLogWriterCloser is a debug file handle that writes all the TLS
// session keys. This value should only be non-nil during debugging.
tlsKeyLogWriterCloser io.WriteCloser
// this node's initial connections to the network
bootstrappers nodevalidators.Manager
// current validators of the network
vdrs nodevalidators.Manager
apiURI string
// Handles HTTP API calls
APIServer server.Server
// This node's configuration
Config *node.Config
tracer trace.Tracer
// ensures that we only close the node once.
shutdownOnce sync.Once
// True if node is shutting down or is done shutting down
shuttingDown utils.Atomic[bool]
// Sets the exit code
shuttingDownExitCode utils.Atomic[int]
// Metrics Registerer
MetricsGatherer metric.MultiGatherer
MeterDBMetricsGatherer metric.MultiGatherer
VMAliaser ids.Aliaser
VMManager vms.Manager
// VM endpoint registry
VMRegistry registry.VMRegistry
// Manages shutdown of a VM process
runtimeManager runtime.Manager
resourceManager resource.Manager
// Tracks the CPU/disk usage caused by processing
// messages of each peer.
resourceTracker tracker.ResourceTracker
// Specifies how much CPU usage each peer can cause before
// we rate-limit them.
cpuTargeter tracker.Targeter
// Specifies how much disk usage each peer can cause before
// we rate-limit them.
diskTargeter tracker.Targeter
// Closed when a sufficient amount of bootstrap nodes are connected to
onSufficientlyConnected chan struct{}
}
/*
******************************************************************************
*************************** P2P Networking Section ***************************
******************************************************************************
*/
// Initialize the networking layer.
// Assumes [n.vdrs], [n.CPUTracker], and [n.CPUTargeter] have been initialized.
func (n *Node) initNetworking(reg metric.Registerer) error {
// Providing either loopback address - `::1` for ipv6 and `127.0.0.1` for ipv4 - as the listen
// host will avoid the need for a firewall exception on recent MacOS:
//
// - MacOS requires a manually-approved firewall exception [1] for each version of a given
// binary that wants to bind to all interfaces (i.e. with an address of `:[port]`). Each
// compiled version of node requires a separate exception to be allowed to bind to all
// interfaces.
//
// - A firewall exception is not required to bind to a loopback interface, but the only way for
// Listen() to bind to loopback for both ipv4 and ipv6 is to bind to all interfaces [2] which
// requires an exception.
//
// - Thus, the only way to start a node on MacOS without approving a firewall exception for the
// node binary is to bind to loopback by specifying the host to be `::1` or `127.0.0.1`.
//
// 1: https://apple.stackexchange.com/questions/393715/do-you-want-the-application-main-to-accept-incoming-network-connections-pop
// 2: https://github.com/golang/go/issues/56998
listenAddress := net.JoinHostPort(n.Config.ListenHost, strconv.FormatUint(uint64(n.Config.ListenPort), 10))
listener, err := net.Listen(constants.NetworkType, listenAddress)
if err != nil {
return err
}
// Wrap listener so it will only accept a certain number of incoming connections per second
listener = throttling.NewThrottledListener(listener, n.Config.NetworkConfig.ThrottlerConfig.MaxInboundConnsPerSec)
// Record the bound address to enable inclusion in process context file.
n.stakingAddress, err = endpoints.ParseAddrPort(listener.Addr().String())
if err != nil {
return err
}
var (
stakingPort = n.stakingAddress.Port()
publicAddr netip.Addr
atomicIP *utils.Atomic[netip.AddrPort]
)
switch {
case n.Config.PublicIP != "":
// Use the specified public IP.
publicAddr, err = endpoints.ParseAddr(n.Config.PublicIP)
if err != nil {
return fmt.Errorf("invalid public IP address %q: %w", n.Config.PublicIP, err)
}
atomicIP = utils.NewAtomic(netip.AddrPortFrom(
publicAddr,
stakingPort,
))
n.ipUpdater = dynamicip.NewNoUpdater()
case n.Config.PublicIPResolutionService != "":
// Use dynamic IP resolution.
resolver, err := dynamicip.NewResolver(n.Config.PublicIPResolutionService)
if err != nil {
return fmt.Errorf("couldn't create IP resolver: %w", err)
}
// Use that to resolve our public IP.
ctx, cancel := context.WithTimeout(context.Background(), ipResolutionTimeout)
publicAddr, err = resolver.Resolve(ctx)
cancel()
if err != nil {
return fmt.Errorf("couldn't resolve public IP: %w", err)
}
atomicIP = utils.NewAtomic(netip.AddrPortFrom(
publicAddr,
stakingPort,
))
n.ipUpdater = dynamicip.NewUpdater(atomicIP, resolver, n.Config.PublicIPResolutionFreq)
default:
publicAddr, err = n.router.ExternalIP()
if err != nil {
return fmt.Errorf("public IP / IP resolution service not given and failed to resolve IP with NAT: %w", err)
}
atomicIP = utils.NewAtomic(netip.AddrPortFrom(
publicAddr,
stakingPort,
))
n.ipUpdater = dynamicip.NewNoUpdater()
}
if !endpoints.IsPublic(publicAddr) {
n.Log.Warn("P2P IP is private, you will not be publicly discoverable",
"ip", publicAddr,
)
}
// Regularly update our public IP and port mappings.
n.portMapper.Map(
stakingPort,
stakingPort,
stakingPortName,
atomicIP,
n.Config.PublicIPResolutionFreq,
)
go n.ipUpdater.Dispatch(n.Log)
n.Log.Info("initializing networking",
"ip", atomicIP.Get(),
)
tlsKey, ok := n.Config.StakingTLSCert.PrivateKey.(crypto.Signer)
if !ok {
return errInvalidTLSKey
}
// Publish the staking TLS private key as the node's block signer. The chain
// manager passes this to proposervm as StakingLeafSigner so the elected
// proposer can SIGN post-fork blocks (block.Build → key.Sign). It was
// declared but never assigned, so proposervm received a nil signer and
// panicked (nil pointer in key.Sign) the moment it built the first signed
// post-fork block — mirrors avalanchego setting StakingTLSSigner from the
// cert's private key.
n.StakingTLSSigner = tlsKey
if n.Config.NetworkConfig.TLSKeyLogFile != "" {
n.tlsKeyLogWriterCloser, err = perms.Create(n.Config.NetworkConfig.TLSKeyLogFile, perms.ReadWrite)
if err != nil {
return err
}
n.Log.Warn("TLS key logging is enabled",
"filename", n.Config.NetworkConfig.TLSKeyLogFile,
)
}
// We allow nodes to gossip unknown LPs in case the current LPs constant
// becomes out of date.
var unknownLPs set.Set[uint32]
for lp := range n.Config.NetworkConfig.SupportedLPs {
if !constants.CurrentLPs.Contains(lp) {
unknownLPs.Add(lp)
}
}
for lp := range n.Config.NetworkConfig.ObjectedLPs {
if !constants.CurrentLPs.Contains(lp) {
unknownLPs.Add(lp)
}
}
if unknownLPs.Len() > 0 {
n.Log.Warn("gossiping unknown LPs",
"lps", unknownLPs,
)
}
tlsConfig := peer.TLSConfig(n.Config.StakingTLSCert, n.tlsKeyLogWriterCloser)
// Create chain router
// Note: passing nil for timeoutManager - SimpleRouter doesn't strictly need it for health checks
n.chainRouter = NewSimpleRouter(n.Log, nil)
// Configure benchlist
benchlistGatherer := metrics.NewLabelGatherer(chains.ChainLabel)
// Don't assign to metric.DefaultRegisterer - it requires metric.Registerer interface
err = n.MetricsGatherer.Register(
benchlistNamespace,
benchlistGatherer,
)
if err != nil {
return err
}
// Create benchlist manager
n.benchlistManager = benchlist.NewManager(n.Log, metric.NewRegistry(), &benchlist.Config{})
n.uptimeCalculator = uptime.NewLockedCalculator()
consensusRouter := n.chainRouter
if !n.Config.SybilProtectionEnabled {
// Without sybil protection there's no on-chain txID that registered us
// as a validator, so we derive a synthetic one from our NodeID.
dummyTxID := ids.Empty
copy(dummyTxID[:], n.ID.Bytes())
err := n.vdrs.AddStaker(
constants.PrimaryNetworkID,
n.ID,
bls.PublicKeyToUncompressedBytes(n.Config.StakingSigningKey.PublicKey()),
dummyTxID,
n.Config.SybilProtectionDisabledWeight,
)
if err != nil {
return err
}
}
// Create unified validator manager
n.onSufficientlyConnected = make(chan struct{})
numBootstrappers := n.bootstrappers.NumValidators(constants.PrimaryNetworkID)
requiredConns := int64((3*numBootstrappers + 3) / 4)
if requiredConns == 0 {
close(n.onSufficientlyConnected)
}
// Get tracked chain IDs for validator manager
var trackedNetworkIDs []ids.ID
if n.Config.TrackedChains != nil {
trackedNetworkIDs = n.Config.TrackedChains.List()
}
consensusRouter = NewValidatorManager(ValidatorManagerConfig{
Router: consensusRouter,
Log: n.Log,
Validators: n.vdrs,
Beacons: n.bootstrappers,
TrackedNetworks: trackedNetworkIDs,
SybilProtectionDisabled: !n.Config.SybilProtectionEnabled,
SybilProtectionWeight: n.Config.SybilProtectionDisabledWeight,
RequiredBeaconConns: requiredConns,
OnSufficientlyConnected: n.onSufficientlyConnected,
})
// add node configs to network config
n.Config.NetworkConfig.MyNodeID = n.ID
// Mirror the strict-PQ staking ML-DSA keypair so the network layer's
// PQ peer handshake signs with the SAME key that derives MyNodeID
// (see network.NewNetwork -> peer.NewLocalIdentityFromStakingKey).
// Nil on classical-compat chains, where the PQ handshake is skipped.
n.Config.NetworkConfig.StakingMLDSA = n.Config.StakingConfig.StakingMLDSA
n.Config.NetworkConfig.StakingMLDSAPub = n.Config.StakingConfig.StakingMLDSAPub
n.Config.NetworkConfig.MyIPPort = atomicIP
n.Config.NetworkConfig.NetworkID = n.Config.NetworkID
n.Config.NetworkConfig.Validators = n.vdrs
n.Config.NetworkConfig.Beacons = n.bootstrappers
n.Config.NetworkConfig.TLSConfig = tlsConfig
n.Config.NetworkConfig.TLSKey = tlsKey
n.Config.NetworkConfig.BLSKey = n.Config.StakingSigningKey
n.Config.NetworkConfig.TrackedChains = n.Config.TrackedChains
n.Config.NetworkConfig.UptimeCalculator = n.uptimeCalculator
n.Config.NetworkConfig.UptimeRequirement = n.Config.StakingConfig.UptimeRequirement
// Wrap the resource tracker for consensus compatibility
n.Config.NetworkConfig.ResourceTracker = &resourceTrackerAdapter{tracker: n.resourceTracker}
n.Config.NetworkConfig.CPUTargeter = n.cpuTargeter
n.Config.NetworkConfig.DiskTargeter = n.diskTargeter
n.Config.NetworkConfig.GenesisBytes = n.Config.GenesisBytes
// Forward the resolved ChainSecurityProfile to the network layer
// so the peer.SchemeGate (CR-3) and PQ handshake (CR-5) fire on
// strict-PQ chains. Nil on legacy / classical-compat networks.
n.Config.NetworkConfig.SecurityProfile = n.securityProfile
// Map native chains (P/C/X/etc.) to the primary network validator set.
// For non-primary chains, return ids.Empty to let blockchainToNetwork map resolve
// the correct chain ID. Returning chainID here would short-circuit the
// lookup and cause chain messages to be sequenced under the wrong ID,
// preventing block propagation to other nodes.
n.Config.NetworkConfig.SequencerIDForChain = func(chainID ids.ID) ids.ID {
if ids.IsNativeChain(chainID) {
return constants.PrimaryNetworkID
}
return ids.Empty
}
// Wrap the router to implement network.ExternalHandler
externalHandler := &externalHandlerWrapper{router: consensusRouter}
// Create a Registry for network metrics
networkRegistry := metric.NewRegistry()
n.Net, err = network.NewNetwork(
&n.Config.NetworkConfig,
upgrade.InitiallyActiveTime,
n.msgCreator,
networkRegistry,
n.Log,
listener,
dialer.NewEndpointDialer(constants.NetworkType, dialer.EndpointDialerConfig{
Config: n.Config.NetworkConfig.DialerConfig,
DNSConfig: dialer.DefaultDNSCacheConfig(),
}, n.Log),
externalHandler,
)
return err
}
// Write process context to the configured path. Supports the use of
// dynamically chosen network ports with local network orchestration.
func (n *Node) writeProcessContext() error {
n.Log.Info("writing process context", "path", n.Config.ProcessContextFilePath)
// Write the process context to disk
processContext := &node.ProcessContext{
PID: os.Getpid(),
URI: n.apiURI,
StakingAddress: n.stakingAddress, // Set by network initialization
}
bytes, err := json.Marshal(processContext, jsontext.WithIndent(" "))
if err != nil {
return fmt.Errorf("failed to marshal process context: %w", err)
}
if err := perms.WriteFile(n.Config.ProcessContextFilePath, bytes, perms.ReadWrite); err != nil {
return fmt.Errorf("failed to write process context: %w", err)
}
return nil
}
// Dispatch starts the node's servers.
// Returns when the node exits.
func (n *Node) Dispatch() error {
if err := n.writeProcessContext(); err != nil {
return err
}
// Start the HTTP API server
go func() {
defer func() {
if r := recover(); r != nil {
n.Log.Error("panic in API server", "panic", r)
}
}()
n.Log.Info("API server listening",
"uri", n.apiURI,
)
err := n.APIServer.Dispatch()
// When [n].Shutdown() is called, [n.APIServer].Close() is called.
// This causes [n.APIServer].Dispatch() to return an error.
// If that happened, don't log/return an error here.
if !n.shuttingDown.Get() {
n.Log.Error("API server dispatch failed",
"error", err,
)
}
// If the API server isn't running, shut down the node.
// If node is already shutting down, this does not tigger shutdown again,
// and blocks until Shutdown returns.
n.Shutdown(1)
}()
// Log a warning if we aren't able to connect to a sufficient portion of
// nodes.
go func() {
timer := time.NewTimer(n.Config.BootstrapBeaconConnectionTimeout)
defer timer.Stop()
select {
case <-timer.C:
if n.shuttingDown.Get() {
return
}
n.Log.Warn("failed to connect to bootstrap nodes",
"bootstrappers", "nodevalidators.Manager",
"duration", n.Config.BootstrapBeaconConnectionTimeout,
)
case <-n.onSufficientlyConnected:
}
}()
// Add state sync nodes to the peer network
for i, peerIP := range n.Config.StateSyncIPs {
n.Net.ManuallyTrack(n.Config.StateSyncIDs[i], endpoints.NewIPEndpoint(peerIP))
}
// Add bootstrap nodes to the peer network
n.Log.Info("Adding bootstrap nodes to peer network",
"count", len(n.Config.Bootstrappers),
)
for i, bootstrapper := range n.Config.Bootstrappers {
if bootstrapper.ID == ids.EmptyNodeID {
// Endpoint-only bootstrap node (--bootstrap-nodes flag).
// NodeID will be discovered from the peer's staking certificate
// during the TLS handshake. ManuallyTrack with empty ID triggers
// endpoint-discovery mode.
n.Log.Info("Bootstrap node (endpoint-only, ID from cert)",
"index", i,
"endpoint", bootstrapper.Endpoint.String(),
)
} else {
n.Log.Info("Bootstrap node",
"index", i,
"nodeID", bootstrapper.ID.String(),
"endpoint", bootstrapper.Endpoint.String(),
)
}
n.Net.ManuallyTrack(bootstrapper.ID, bootstrapper.Endpoint)
}
n.Log.Info("Finished adding bootstrap nodes, starting Dispatch")
// Start P2P connections
retErr := n.Net.Dispatch()
// If the P2P server isn't running, shut down the node.
// If node is already shutting down, this does not tigger shutdown again,
// and blocks until Shutdown returns.
n.Shutdown(1)
if n.tlsKeyLogWriterCloser != nil {
err := n.tlsKeyLogWriterCloser.Close()
if err != nil {
n.Log.Error("closing TLS key log file failed",
"filename", n.Config.NetworkConfig.TLSKeyLogFile,
"error", err,
)
}
}
// Remove the process context file to communicate to an orchestrator
// that the node is no longer running.
if err := os.Remove(n.Config.ProcessContextFilePath); err != nil && !errors.Is(err, fs.ErrNotExist) {
n.Log.Error("removal of process context file failed",
"path", n.Config.ProcessContextFilePath,
"error", err,
)
}
return retErr
}
/*
******************************************************************************
*********************** End P2P Networking Section ***************************
******************************************************************************
*/
func (n *Node) initDatabase() error {
// All databases use the same folder structure now (zapdb is default)
dbFolderName := "db"
// dbFolderName is appended to the database path given in the config
dbFullPath := filepath.Join(n.Config.DatabaseConfig.Path, dbFolderName)
var err error
n.DB, err = databasefactory.New(
n.Config.DatabaseConfig.Name,
dbFullPath,
n.Config.DatabaseConfig.ReadOnly,
n.Config.DatabaseConfig.Config,
n.MetricsGatherer,
n.Log,
dbNamespace,
"all",
)
if err != nil {
return fmt.Errorf("couldn't create database: %w", err)
}
rawExpectedGenesisHash := hash.ComputeHash256(n.Config.GenesisBytes)
rawGenesisHash, err := n.DB.Get(genesisHashKey)
if err == database.ErrNotFound {
rawGenesisHash = rawExpectedGenesisHash
err = n.DB.Put(genesisHashKey, rawGenesisHash)
}
if err != nil {
return err
}
genesisHash, err := ids.ToID(rawGenesisHash)
if err != nil {
return err
}
expectedGenesisHash, err := ids.ToID(rawExpectedGenesisHash)
if err != nil {
return err
}
if genesisHash != expectedGenesisHash {
// Sovereign-L1 invariant: the operator owns the chainset; if the
// generated genesis disagrees with the stored hash, the operator
// changed it on purpose (initial bootstrap, validator-set rotation,
// or chainset upgrade) and we silently advance. No --allow-genesis-update
// flag, no manual migration step — the stored hash is a tag, not
// a lock.
n.Log.Info("genesis hash advanced, updating stored hash",
"oldHash", genesisHash,
"newHash", expectedGenesisHash,
)
if err := n.DB.Put(genesisHashKey, rawExpectedGenesisHash); err != nil {
return fmt.Errorf("failed to update genesis hash: %w", err)
}
}
n.Log.Info("initializing database",
"genesisHash", genesisHash,
)
ok, err := n.DB.Has(ungracefulShutdown)
if err != nil {
return fmt.Errorf("failed to read ungraceful shutdown key: %w", err)
}
if ok {
n.Log.Warn("detected previous ungraceful shutdown")
}
if err := n.DB.Put(ungracefulShutdown, nil); err != nil {
return fmt.Errorf(
"failed to write ungraceful shutdown key at: %w",
err,
)
}
return nil
}
// Set the node IDs of the peers this node should first connect to
func (n *Node) initBootstrappers() error {
n.bootstrappers = nodevalidators.NewManager()
for _, bootstrapper := range n.Config.Bootstrappers {
// Skip endpoint-only bootstrappers — their NodeID is discovered
// from the peer's staking certificate during the TLS handshake
// and added to the bootstrapper set dynamically.
if bootstrapper.ID == ids.EmptyNodeID {
continue
}
// Note: The beacon connection manager will treat all beaconIDs as
// equal.
// Invariant: We never use the TxID or BLS keys populated here.
if err := n.bootstrappers.AddStaker(constants.PrimaryNetworkID, bootstrapper.ID, nil, ids.Empty, 1); err != nil {
return err
}
}
return nil
}
// SecurityProfile returns the chain-wide ChainSecurityProfile this node
// is operating under, or nil when the genesis carries no pin. The
// returned pointer is the post-Validate+ComputeHash result already
// stamped with ProfileHash; callers MUST NOT mutate it.
//
// Downstream consumers (signer factory, peer handshake gate, mempool
// admittance, validator registry, bridge oracle co-signer) take this
// as a dependency at construction time. The single load happens at
// node bootstrap (initSecurityProfile); there is no per-request
// re-resolution.
//
// Closes red-team finding F102 at the consumer API boundary.
func (n *Node) SecurityProfile() *consensusconfig.ChainSecurityProfile {
return n.securityProfile
}
// initSecurityProfile loads the chain-wide ChainSecurityProfile from
// the genesis pin. Called once during New(). Refuses to start the node
// if a pin is present but its content hash does not match the live
// canonical profile from consensus/config — a forked binary that
// swapped in a different canonical content would otherwise pass the
// ProfileID check silently.
//
// When the genesis file carries no pin (legacy networks pre-locked-
// profile), this is a logged warning, not a fatal error: the node
// boots in classical-compat mode and downstream consumers see a nil
// SecurityProfile().
//
// Closes red-team finding F102 at the node bootstrap layer.
func (n *Node) initSecurityProfile() error {
cfg := genesiscfg.GetConfig(n.Config.NetworkID)
if cfg == nil {
n.Log.Warn("genesis config not found — node boots in classical-compat mode")
return nil
}
return n.applySecurityProfile(cfg.SecurityProfile)
}
// applySecurityProfile is the test-and-prod common path: take the
// genesiscfg.SecurityProfile pin from a genesis config struct, resolve
// it through consensus/config.ProfileByID + ComputeHash verification,
// stamp the result onto n.securityProfile, and emit the startup banner.
// Split out so unit tests can exercise the resolve+banner path without
// touching the filesystem-based genesiscfg.GetConfig.
//
// The startup banner format is fixed (callers grep on these strings):
//
// SECURITY PROFILE: <name>
// PROFILE HASH: <hex>
// POST-QUANTUM: <true|false>
// NIST-FRIENDLY: <true|false>
// CLASSICAL SNARKS: <forbidden|allowed>
// BLS FALLBACK: <forbidden|allowed>
func (n *Node) applySecurityProfile(pin *genesiscfg.SecurityProfile) error {
if pin == nil {
n.Log.Warn("genesis carries no SecurityProfile pin — node boots in classical-compat mode")
return nil
}
profile, err := genesissecurity.ResolveProfile(pin)
if err != nil {
return fmt.Errorf("genesis SecurityProfile failed to resolve: %w", err)
}
n.securityProfile = profile
// Startup banner. Format mirrors the one in the F102 task spec so
// audit tooling and grep scripts can rely on it.
classicalSNARKs := "allowed"
if profile.ForbidClassicalSNARKs {
classicalSNARKs = "forbidden"
}
blsFallback := "allowed"
if profile.ForbidPairings ||
profile.ProfileID == uint32(consensusconfig.ProfileStrictPQ) ||
profile.ProfileID == uint32(consensusconfig.ProfileFIPS) {
blsFallback = "forbidden"
}
postQuantum := profile.ProofPolicyID.IsPostQuantum()
nistFriendly := profile.HashSuiteID == consensusconfig.HashSuiteSHA3NIST
hashHex := fmt.Sprintf("%x", profile.ProfileHash[:])
n.Log.Info("SECURITY PROFILE: " + profile.ProfileName)
n.Log.Info("PROFILE HASH: " + hashHex)
n.Log.Info(fmt.Sprintf("POST-QUANTUM: %v", postQuantum))
n.Log.Info(fmt.Sprintf("NIST-FRIENDLY: %v", nistFriendly))
n.Log.Info("CLASSICAL SNARKS: " + classicalSNARKs)
n.Log.Info("BLS FALLBACK: " + blsFallback)
return nil
}
// Create the EventDispatcher used for hooking events
// into the general process flow.
func (n *Node) initEventDispatchers() {
n.BlockAcceptorGroup = nodeconsensus.NewAcceptorGroup(n.Log)
n.TxAcceptorGroup = nodeconsensus.NewAcceptorGroup(n.Log)
n.VertexAcceptorGroup = nodeconsensus.NewAcceptorGroup(n.Log)
}
// Initialize [n.indexer].
// Should only be called after [n.DB], [n.DecisionAcceptorGroup],
// [n.ConsensusAcceptorGroup], [n.Log], [n.APIServer], [n.chainManager] are
// initialized
func (n *Node) initIndexer() error {
txIndexerDB := prefixdb.New(indexerDBPrefix, n.DB)
var err error
n.indexer, err = indexer.NewIndexer(indexer.Config{
IndexingEnabled: n.Config.IndexAPIEnabled,
AllowIncompleteIndex: n.Config.IndexAllowIncomplete,
DB: txIndexerDB,
Log: n.Log,
BlockAcceptorGroup: n.BlockAcceptorGroup,
TxAcceptorGroup: n.TxAcceptorGroup,
VertexAcceptorGroup: n.VertexAcceptorGroup,
APIServer: n.APIServer,
ShutdownF: func() {
n.Shutdown(0)
},
})
if err != nil {
return fmt.Errorf("couldn't create index for txs: %w", err)
}
// Chain manager will notify indexer when a chain is created
n.chainManager.AddRegistrant(n.indexer)
return nil
}
// Initializes the Platform chain.
// Its genesis data specifies the other chains that should be created.
func (n *Node) initChains(genesisBytes []byte) error {
n.Log.Info("initializing chains")
platformChain := chains.ChainParameters{
ID: constants.PlatformChainID,
ChainID: constants.PrimaryNetworkID,
GenesisData: genesisBytes, // Specifies other chains to create
VMID: constants.PlatformVMID,
CustomBeacons: n.bootstrappers,
}
// Start the chain creator with the Platform Chain
return n.chainManager.StartChainCreator(platformChain)
}
func (n *Node) initMetrics() error {
n.MetricsGatherer = metric.NewPrefixGatherer()
n.MeterDBMetricsGatherer = metrics.NewLabelGatherer(chains.ChainLabel)
return n.MetricsGatherer.Register(
meterDBNamespace,
n.MeterDBMetricsGatherer,
)
}
func (n *Node) initNAT() {
n.Log.Info("initializing NAT")
if n.Config.PublicIP == "" && n.Config.PublicIPResolutionService == "" {
n.router = nat.GetRouter()
if !n.router.SupportsNAT() {
n.Log.Warn("UPnP and NAT-PMP router attach failed, " +
"you may not be listening publicly. " +
"Please confirm the settings in your router")
}
} else {
n.router = nat.NewNoRouter()
}
n.portMapper = nat.NewPortMapper(n.Log, n.router)
}
// initAPIServer initializes the server that handles HTTP calls
func (n *Node) initAPIServer() error {
n.Log.Info("initializing API server")
// An empty host is treated as a wildcard to match all addresses, so it is
// considered public.
hostIsPublic := n.Config.HTTPHost == ""
if !hostIsPublic {
ip, err := endpoints.Lookup(n.Config.HTTPHost)
if err != nil {
n.Log.Error("failed to lookup HTTP host",
"host", n.Config.HTTPHost,
"error", err,
)
return err
}
hostIsPublic = endpoints.IsPublic(ip)
n.Log.Debug("finished HTTP host lookup",
"host", n.Config.HTTPHost,
"ip", ip,
"isPublic", hostIsPublic,
)
}
listenAddress := net.JoinHostPort(n.Config.HTTPHost, strconv.FormatUint(uint64(n.Config.HTTPPort), 10))
listener, err := net.Listen("tcp", listenAddress)
if err != nil {
return err
}
addrStr := listener.Addr().String()
addrPort, err := endpoints.ParseAddrPort(addrStr)
if err != nil {
return err
}
// Don't open the HTTP port if the HTTP server is private
if hostIsPublic {
n.Log.Warn("HTTP server is binding to a potentially public host. "+
"You may be vulnerable to a DoS attack if your HTTP port is publicly accessible",
"host", n.Config.HTTPHost,
)
n.portMapper.Map(
addrPort.Port(),
addrPort.Port(),
httpPortName,
nil,
n.Config.PublicIPResolutionFreq,
)
}
protocol := "http"
if n.Config.HTTPSEnabled {
cert, err := tls.X509KeyPair(n.Config.HTTPSCert, n.Config.HTTPSKey)
if err != nil {
return err
}
config := &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cert},
}
listener = tls.NewListener(listener, config)
protocol = "https"
}
n.apiURI = fmt.Sprintf("%s://%s", protocol, listener.Addr())
apiRegisterer, err := metric.MakeAndRegister(
n.MetricsGatherer,
apiNamespace,
)
if err != nil {
return err
}
n.APIServer, err = server.New(
n.Log,
listener,
n.Config.HTTPAllowedOrigins,
n.Config.ShutdownTimeout,
n.ID,
n.Config.TraceConfig.ExporterConfig.Type != trace.Disabled,
n.tracer,
apiRegisterer,
n.Config.HTTPConfig.HTTPConfig,
n.Config.HTTPAllowedHosts,
)
return err
}
// Add the default VM aliases
func (n *Node) addDefaultVMAliases() error {
n.Log.Info("adding the default VM aliases")
for vmID, aliases := range builder.VMAliases {
for _, alias := range aliases {
if err := n.VMAliaser.Alias(vmID, alias); err != nil {
return err
}
}
}
return nil
}
// Create the chainManager and register the following VMs:
// XVM, Simple Payments DAG, Simple Payments Chain, and Platform VM
// Assumes n.DBManager, n.vdrs all initialized (non-nil)
func (n *Node) initChainManager(utxoAssetID ids.ID) error {
// X-Chain (XVM, the historic exchange chain) is OPT-IN. Networks that
// don't bake an XVM into platform genesis run in "P-only" mode where
// asset creation and UTXO ops are first-class on the P-Chain (see
// CreateAssetTx/OperationTx in vms/platformvm/txs). All cross-chain
// flows that historically went P→X→C are replaced with direct
// P→chain warp transfers.
var xChainID ids.ID
if createXVMTx, err := builder.VMGenesis(n.Config.GenesisBytes, constants.XVMID); err == nil {
xChainID = createXVMTx.ID()
} else {
n.Log.Info("X-Chain not in genesis — skipping",
"reason", "platform genesis has no constants.XVMID chain (P-only mode)")
}
// Primary-network EVM (the historic "C-Chain") is OPT-IN. Networks
// that don't bake an EVM into platform genesis (because they register
// their own EVM as a dedicated chain via `platform.createChainTx`)
// leave cChainID = ids.Empty. The chain manager and aliasing logic
// already handle the empty ID case (see chains/manager.go).
var cChainID ids.ID
if createEVMTx, err := builder.VMGenesis(n.Config.GenesisBytes, constants.EVMID); err == nil {
cChainID = createEVMTx.ID()
} else {
n.Log.Info("C-Chain (primary-network EVM) not in genesis — skipping",
"reason", "platform genesis has no constants.EVMID chain")
}
// P-Chain is always critical regardless of configuration.
criticalChains := set.Of(constants.PlatformChainID)
// Q-Chain (Quantum) is critical by default — quantum finality requires it.
if createQVMTx, err := builder.VMGenesis(n.Config.GenesisBytes, constants.QuantumVMID); err == nil {
qChainID := createQVMTx.ID()
criticalChains.Add(qChainID)
n.Log.Info("Q-Chain critical for quantum finality", "chainID", qChainID)
}
// Resolve D-Chain ID for the ManagerConfig even if D is not critical.
// The DEXVM factory loads from PluginDir (CGO=0 plugin built by the
// Dockerfile Chain VM Plugin Stage), so the ID derived from genesis is
// always meaningful regardless of in-process registration. Whether this
// node ACTIVATES the D-Chain DEX (tracks/validates it + dials the venue)
// is enforced downstream by the chain manager's authorizeChainActivation
// from ManagerConfig.DexValidator (wired below). participatesInDEXChain
// reads the same flag only to log the operator's intent here.
var dChainID ids.ID
if createDexVMTx, err := builder.VMGenesis(n.Config.GenesisBytes, constants.DexVMID); err == nil {
dChainID = createDexVMTx.ID()
if n.participatesInDEXChain() {
n.Log.Info("DEX validator: D-Chain activation enabled (dex-validator=true)", "chainID", dChainID)
} else {
n.Log.Info("D-Chain in genesis but activation disabled (dex-validator=false) — not tracked even under --track-all-chains", "chainID", dChainID)
}
} else {
n.Log.Info("D-Chain not in genesis, skipping")
}
_, err := metric.MakeAndRegister(
n.MetricsGatherer,
requestsNamespace,
)
if err != nil {
return err
}
_, err = metric.MakeAndRegister(
n.MetricsGatherer,
responsesNamespace,
)
if err != nil {
return err
}
// Create timeout manager with a default timeout
n.timeoutManager = timeout.NewManager(30 * time.Second)
netsManager, err := chains.NewNets(n.ID, n.Config.NetConfigs)
if err != nil {
return fmt.Errorf("failed to initialize chains: %w", err)
}
n.chainManager, err = chains.New(
&chains.ManagerConfig{
SybilProtectionEnabled: n.Config.SybilProtectionEnabled,
StakingTLSSigner: n.StakingTLSSigner,
StakingTLSCert: n.StakingTLSCert,
StakingMLDSASigner: n.Config.StakingConfig.StakingMLDSA,
StakingMLDSAPub: n.Config.StakingConfig.StakingMLDSAPub,
ProposerWindowDuration: n.Config.ProposerWindowDuration,
ProposerMinBlockDelay: n.Config.ProposerMinBlockDelay,
StakingBLSKey: n.Config.StakingSigningKey,
Log: n.Log,
LogFactory: n.LogFactory,
VMManager: n.VMManager,
BlockAcceptorGroup: n.BlockAcceptorGroup,
TxAcceptorGroup: n.TxAcceptorGroup,
VertexAcceptorGroup: n.VertexAcceptorGroup,
DB: n.DB,
MsgCreator: n.msgCreator,
Router: n.chainRouter,
Net: n.Net,
Validators: n.vdrs,
PartialSyncPrimaryNetwork: n.Config.PartialSyncPrimaryNetwork,
NodeID: n.ID,
NetworkID: n.Config.NetworkID,
Server: n.APIServer,
AtomicMemory: n.sharedMemory,
UTXOAssetID: utxoAssetID,
XChainID: xChainID,
CChainID: cChainID,
DChainID: dChainID,
CriticalChains: criticalChains,
// ChainAuthorizations is derived from the single OptionalVMs
// registry (which VMs need which operator NFT) joined with the
// per-network collection assets in Config.NFTAuthorizationAssets and
// genesis-resolved chain IDs. Empty until a network configures an
// operator collection — so gated optional chains (D/B) behave as
// today and ungated ones are untouched — while the gate itself fails
// closed for any configured-but-unheld NFT (chains/manager_authz.go).
ChainAuthorizations: chainAuthorizationsFor(n.Config.GenesisBytes, n.Config.NFTAuthorizationAssets),
// DexValidator is the operator's D-Chain opt-in. It is enforced as a
// necessary activation condition by authorizeChainActivation (composed
// AND with the NFT gate), so a node WITHOUT dex-validator does not
// activate the D-Chain even under --track-all-chains. Read once here
// (same value participatesInDEXChain reports for the log line below).
DexValidator: n.Config.DexValidator,
TimeoutManager: n.timeoutManager,
Health: n.health,
ShutdownNodeFunc: n.Shutdown,
MeterVMEnabled: n.Config.MeterVMEnabled,
Metrics: n.MetricsGatherer,
MeterDBMetrics: n.MeterDBMetricsGatherer,
ChainConfigs: n.Config.ChainConfigs,
FrontierPollFrequency: n.Config.FrontierPollFrequency,
ConsensusAppConcurrency: n.Config.ConsensusAppConcurrency,
BootstrapMaxTimeGetAncestors: n.Config.BootstrapMaxTimeGetAncestors,
BootstrapAncestorsMaxContainersSent: n.Config.BootstrapAncestorsMaxContainersSent,
BootstrapAncestorsMaxContainersReceived: n.Config.BootstrapAncestorsMaxContainersReceived,
Upgrades: n.Config.UpgradeConfig,
ResourceTracker: n.resourceTracker,
StateSyncBeacons: n.Config.StateSyncIDs,
TracingEnabled: n.Config.TraceConfig.ExporterConfig.Type != trace.Disabled,
Tracer: n.tracer,
ChainDataDir: filepath.Join(n.Config.ChainDataDir, fmt.Sprintf("network-%d", n.Config.NetworkID)),
Nets: netsManager,
SkipBootstrap: n.Config.SkipBootstrap,
EnableAutomining: n.Config.EnableAutomining,
// F118: forward the chain-wide ChainSecurityProfile to the
// chain manager so it can stamp the profile pin into every
// C-Chain (coreth) plugin Initialize payload.
SecurityProfile: n.securityProfile,
},
)
if err != nil {
return err
}
// Notify the API server when new chains are created
n.chainManager.AddRegistrant(n.APIServer)
return nil
}
// classicalCompatRegistry returns the strict-PQ bootstrap escape hatch.
//
// On a strict-PQ chain the platformvm/xvm mempool refuses every classical
// secp256k1 credential (auth.EnforceCredentialPolicy). That makes the
// chain un-bootstrappable: the chain-creation tooling signs CreateNetworkTx
// /CreateChainTx with a classical secp256k1 control key, and with an empty
// allow-list those txs can never be admitted. To break the cycle we seed
// the allow-list with the genesis-funded P-chain allocation owners — the
// bootstrap trust root that the genesis itself already vouches for — so
// those keys (and only those keys) may sign classical credentials during
// bootstrap.
//
// This is a devnet bootstrap escape hatch. Production should narrow this
// to a governance-managed allow-list rather than blanket-trusting every
// genesis allocation owner.
//
// Returns nil when the chain is NOT strict-PQ (legacy/classical path), in
// which case the mempool admits classical credentials unconditionally and
// no allow-list is needed.
func (n *Node) classicalCompatRegistry() auth.ClassicalCompatRegistry {
if n.securityProfile == nil || !n.securityProfile.RequireTypedTxAuth {
return nil
}
gen, err := platformvmgenesis.Parse(n.Config.GenesisBytes)
if err != nil {
// GenesisBytes already parsed cleanly earlier in bootstrap; a
// failure here is a wiring bug. Refuse to silently ship an empty
// allow-list (which would brick chain creation) and instead fall
// back to nil so the operator sees the strict-PQ refusal directly.
n.Log.Error("strict-PQ: failed to parse platform genesis for classical-compat allow-list", "error", err)
return nil
}
seen := set.NewSet[ids.ShortID](len(gen.UTXOs))
addrs := make([]ids.ShortID, 0, len(gen.UTXOs))
for _, utxo := range gen.UTXOs {
// Every genesis output type (secp256k1fx.TransferOutput and the
// stakeable.LockOut wrapper) implements lux.Addressable, so we
// collect owners without type-switching on concrete output types.
addressable, ok := utxo.Out.(lux.Addressable)
if !ok {
continue
}
for _, raw := range addressable.Addresses() {
addr, err := ids.ToShortID(raw)
if err != nil {
n.Log.Warn("strict-PQ: skipping malformed genesis allocation owner", "error", err)
continue
}
if seen.Contains(addr) {
continue
}
seen.Add(addr)
addrs = append(addrs, addr)
}
}
// The platformvm/xvm mempool does not yet resolve the per-tx originator —
// auth.EnforceCredentialPolicy is invoked with ids.ShortEmpty (see
// vms/platformvm/txs/mempool/mempool.go). A classical-credentialed P-chain
// tx is therefore admitted iff ids.ShortEmpty is allow-listed. Include it
// so the strict-PQ bootstrap (CreateNetwork/CreateChainTx, signed by a
// genesis-funded classical control key) is admissible. This admits
// classical P-chain txs broadly on this chain; it does NOT touch the
// C-chain/EVM (separate secp256k1 auth) or the strict-PQ consensus
// handshake. FOLLOW-UP: resolve the real originator in the mempool, then
// narrow back to the named genesis-owner allow-list above.
if !seen.Contains(ids.ShortEmpty) {
addrs = append(addrs, ids.ShortEmpty)
}
cb58 := make([]string, len(addrs))
for i, a := range addrs {
cb58[i] = a.String()
}
n.Log.Info("strict-PQ: seeded classical-compat allow-list from genesis allocation owners",
"count", len(addrs),
"addresses", cb58,
)
return auth.NewStaticClassicalCompatRegistry(addrs)
}
// initVMs initializes the VMs Lux supports + any additional vms installed as plugins.
func (n *Node) initVMs() error {
n.Log.Info("initializing VMs")
vdrs := n.vdrs
// Strict-PQ bootstrap escape hatch: the genesis-funded P-chain
// allocation owners may sign classical secp256k1 credentials so the
// chain-creation control key can issue CreateNetwork/CreateChainTx on a
// strict-PQ L1. Computed once and shared by the platformvm + xvm
// factories below. Nil (legacy behavior) when the chain is not
// strict-PQ. See classicalCompatRegistry for the production caveat.
classicalCompat := n.classicalCompatRegistry()
// If sybil protection is disabled, we provide the P-chain its own local
// validator manager that will not be used by the rest of the node. This
// allows the node's validator sets to be determined by network connections.
if !n.Config.SybilProtectionEnabled {
vdrs = nodevalidators.NewManager()
n.Log.Warn("[VALIDATOR DEBUG] Sybil protection DISABLED - using separate validator manager for PlatformVM")
} else {
n.Log.Info("[VALIDATOR DEBUG] Sybil protection ENABLED - sharing validator manager",
"nodeVdrsPtr", fmt.Sprintf("%p", n.vdrs),
"platformVdrsPtr", fmt.Sprintf("%p", vdrs),
"networkVdrsPtr", fmt.Sprintf("%p", n.Config.NetworkConfig.Validators),
"sameInstance", n.vdrs == n.Config.NetworkConfig.Validators,
)
}
// Register the VMs that Lux supports
err := errors.Join(
n.VMManager.RegisterFactory(context.Background(), constants.PlatformVMID, &platformvm.Factory{
Internal: platformconfig.Internal{
Chains: n.chainManager,
Validators: vdrs,
UptimeLockedCalculator: n.uptimeCalculator,
SybilProtectionEnabled: n.Config.SybilProtectionEnabled,
PartialSyncPrimaryNetwork: n.Config.PartialSyncPrimaryNetwork,
TrackedChains: n.Config.TrackedChains,
TrackAllChains: n.Config.TrackAllChains,
DynamicFeeConfig: n.Config.DynamicFeeConfig,
ValidatorFeeConfig: n.Config.ValidatorFeeConfig,
UptimePercentage: n.Config.UptimeRequirement,
MinValidatorStake: n.Config.MinValidatorStake,
MaxValidatorStake: n.Config.MaxValidatorStake,
MinDelegatorStake: n.Config.MinDelegatorStake,
MinDelegationFee: n.Config.MinDelegationFee,
MinStakeDuration: n.Config.MinStakeDuration,
MaxStakeDuration: n.Config.MaxStakeDuration,
RewardConfig: n.Config.RewardConfig,
UpgradeConfig: n.Config.UpgradeConfig,
UseCurrentHeight: n.Config.UseCurrentHeight,
// F102 close-out: thread the chain-wide profile into the
// P-chain mempool builder. Nil for legacy networks; the
// chain builder MUST set this for strict-PQ chains.
SecurityProfile: n.securityProfile,
// Strict-PQ bootstrap escape hatch: genesis-funded
// allocation owners may sign classical credentials so the
// chain-creation control key can issue CreateNetwork/
// CreateChainTx. Nil for legacy networks. See
// classicalCompatRegistry.
ClassicalCompatRegistry: classicalCompat,
},
}),
// C-Chain (EVM) loaded as plugin via ZAP transport from plugin-dir
)
if err != nil {
n.Log.Error("Failed to register Platform VM", "error", err)
return err
}
n.Log.Info("Platform VM registered successfully")
// Register X-Chain VM (Exchange VM) only if X-Chain is in genesis.
// In P-only mode there is no XVMID chain in genesis; the X-Chain factory
// is therefore not loaded, saving init time and reducing attack surface.
xvmRegistered := false
if _, xErr := builder.VMGenesis(n.Config.GenesisBytes, constants.XVMID); xErr == nil {
n.Log.Info("Registering X-Chain VM", "vmID", constants.XVMID)
err = n.VMManager.RegisterFactory(context.Background(), constants.XVMID, &xvm.Factory{
SecurityProfile: n.securityProfile,
// Strict-PQ bootstrap escape hatch (shared with platformvm
// above); nil for legacy networks. See classicalCompatRegistry.
ClassicalCompatRegistry: classicalCompat,
})
if err != nil {
n.Log.Error("Failed to register X-Chain VM", "error", err)
return err
}
n.Log.Info("X-Chain VM registered successfully")
xvmRegistered = true
} else {
n.Log.Info("X-Chain VM not loaded — P-only mode (CreateAssetTx/OperationTx live on P-Chain)")
}
// C-Chain VM (EVM) is loaded as a plugin via ZAP transport when present
// in genesis. Plugin binary placed at <plugin-dir>/<EVMID> by init container.
// Register the always-activated core VMs in-process (Q, Z). The optional
// chain VMs (A/B/D/G/I/K/O/R/T) load from PluginDir via VMRegistry.Reload
// below, like the C-Chain EVM — keeping luxd small per the all-plugins
// directive.
if err := n.registerCoreVMs(); err != nil {
return err
}
// Anti-shadow guard: with all in-process registration done (P/X above, Q/Z
// in registerCoreVMs), assert that NO optional/plugin VM leaked into the
// in-process registry. This runs BEFORE the PluginDir scan below, so a
// violation aborts boot before any plugin could be shadowed. Fail-closed:
// the node refuses to start rather than silently link a VM that must be a
// plugin.
if err := n.assertNoOptionalShadows(context.Background()); err != nil {
n.Log.Error("VM anti-shadow guard failed", "error", err)
return err
}
// Log summary of registered VMs — derive count from what is actually in
// genesis rather than hard-coding "3" for legacy P+X+C.
coreVMCount := 1 // P-Chain is always present
if xvmRegistered {
coreVMCount++
}
if _, cErr := builder.VMGenesis(n.Config.GenesisBytes, constants.EVMID); cErr == nil {
coreVMCount++ // C-Chain (plugin via ZAP)
}
if _, qErr := builder.VMGenesis(n.Config.GenesisBytes, constants.QuantumVMID); qErr == nil {
coreVMCount++ // Q-Chain (quantum finality)
}
n.Log.Info("═══════════════════════════════════════════════════════════════════")
n.Log.Info("VMs REGISTERED", "core", coreVMCount)
n.Log.Info("───────────────────────────────────────────────────────────────────")
n.Log.Info("P-Chain (Platform): Validators & staking & UTXO assets", "vmID", constants.PlatformVMID)
if xvmRegistered {
n.Log.Info("X-Chain (Exchange): UTXO asset exchange", "vmID", constants.XVMID)
}
if _, cErr := builder.VMGenesis(n.Config.GenesisBytes, constants.EVMID); cErr == nil {
n.Log.Info("C-Chain (Contract): EVM smart contracts (ZAP plugin)", "vmID", constants.EVMID)
}
n.Log.Info("═══════════════════════════════════════════════════════════════════")
// initialize vm runtime manager
n.runtimeManager = runtime.NewManager()
rpcchainvmMetricsGatherer := metrics.NewLabelGatherer(chains.ChainLabel)
if err := n.MetricsGatherer.Register(rpcchainvmNamespace, rpcchainvmMetricsGatherer); err != nil {
return err
}
// initialize the vm registry
n.VMRegistry = registry.NewVMRegistry(registry.VMRegistryConfig{
VMGetter: registry.NewVMGetter(registry.VMGetterConfig{
FileReader: filesystem.NewReader(),
Manager: n.VMManager,
PluginDirectory: n.Config.PluginDir,
ProcessTracker: n.resourceManager,
RuntimeTracker: n.runtimeManager,
MetricsGatherer: rpcchainvmMetricsGatherer,
}),
VMManager: n.VMManager,
})
// register any vms that need to be installed as plugins from disk
reloadCtx, reloadCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer reloadCancel()
_, failedVMs, err := n.VMRegistry.Reload(reloadCtx)
for failedVM, err := range failedVMs {
n.Log.Error("failed to register VM",
"vmID", failedVM,
"error", err,
)
}
if err != nil {
return fmt.Errorf("reload VM registry: %w", err)
}
return nil
}
// initSharedMemory initializes the shared memory for cross chain interation
func (n *Node) initSharedMemory() {
n.Log.Info("initializing SharedMemory")
sharedMemoryDB := prefixdb.New([]byte("shared memory"), n.DB)
n.sharedMemory = atomic.NewMemory(sharedMemoryDB)
}
// initMetricsAPI initializes the Metrics API
// Assumes n.APIServer is already set
func (n *Node) initMetricsAPI() error {
if !n.Config.MetricsAPIEnabled {
n.Log.Info("skipping metrics API initialization because it has been disabled")
return nil
}
if _, err := metric.MakeAndRegister(
n.MetricsGatherer,
processNamespace,
); err != nil {
return err
}
n.Log.Info("initializing metrics API")
return n.APIServer.AddRoute(
metric.HandlerFor(n.MetricsGatherer),
"metrics",
"",
)
}
// initAdminAPI initializes the Admin API service
// Assumes n.log, n.chainManager, and n.ValidatorAPI already initialized
func (n *Node) initAdminAPI() error {
if !n.Config.AdminAPIEnabled {
n.Log.Info("skipping admin API initialization because it has been disabled")
return nil
}
n.Log.Info("initializing admin API")
service := admin.New(admin.Config{
Log: n.Log,
DB: n.DB,
ChainManager: n.chainManager,
HTTPServer: n.APIServer,
ProfileDir: n.Config.ProfilerConfig.Dir,
LogFactory: n.LogFactory,
NodeConfig: n.Config,
VMManager: n.VMManager,
VMRegistry: n.VMRegistry,
PluginDir: n.Config.PluginDir,
Network: n.Net,
DataDir: n.Config.DatabaseConfig.Path,
})
handler, err := service.CreateHandler()
if err != nil {
return err
}
return n.APIServer.AddRoute(
handler,
"admin",
"",
)
}
// initProfiler initializes the continuous profiling
func (n *Node) initProfiler() {
if !n.Config.ProfilerConfig.Enabled {
n.Log.Info("skipping profiler initialization because it has been disabled")
return
}
n.Log.Info("initializing continuous profiler")
n.profiler = profiler.NewContinuous(
filepath.Join(n.Config.ProfilerConfig.Dir, "continuous"),
n.Config.ProfilerConfig.Freq,
n.Config.ProfilerConfig.MaxNumFiles,
)
go func() {
defer func() {
if r := recover(); r != nil {
n.Log.Error("panic in profiler", "panic", r)
}
}()
err := n.profiler.Dispatch()
if err != nil {
n.Log.Error("continuous profiler failed",
"error", err,
)
}
n.Shutdown(1)
}()
}
func (n *Node) initInfoAPI() error {
if !n.Config.InfoAPIEnabled {
n.Log.Info("skipping info API initialization because it has been disabled")
return nil
}
n.Log.Info("initializing info API")
pop, err := signer.NewProofOfPossession(n.Config.StakingSigningKey)
if err != nil {
return fmt.Errorf("problem creating proof of possession: %w", err)
}
service, err := info.NewService(
info.Parameters{
Version: version.CurrentApp,
NodeID: n.ID,
NodePOP: pop,
NetworkID: n.Config.NetworkID,
VMManager: n.VMManager,
Upgrades: n.Config.UpgradeConfig,
TxFee: n.Config.TxFee,
CreateAssetTxFee: n.Config.CreateAssetTxFee,
},
n.Log,
n.vdrs,
n.chainManager,
n.VMManager,
n.Config.NetworkConfig.MyIPPort,
n.Net,
)
if err != nil {
return err
}
return n.APIServer.AddRoute(
service,
"info",
"",
)
}
// initSecurityAPI exposes the chain-wide ChainSecurityProfile as a
// read-only API surface. Three endpoints share one handler:
//
// - JSON-RPC: POST /v1/security with methods securityProfile and
// blockSecurity (dispatched on the wire as security_securityProfile
// / security_blockSecurity per gorilla/rpc namespace convention)
// - REST: GET /v1/security/profile
// - REST: GET /v1/security/block/{n}
//
// All three share the same Service receiver; the shape returned is
// the SCREAMING_SNAKE canonical profile JSON consumed by audit tooling,
// wallet posture banners, and block explorers.
//
// Prometheus gauges for the active profile are stamped onto the
// node-wide metrics gatherer here so /v1/metrics carries the profile
// posture immediately after boot.
//
// Closes F102 follow-ups (securityProfile RPC + profile metrics).
func (n *Node) initSecurityAPI() error {
n.Log.Info("initializing security API")
// Register profile metrics under the "security" namespace on the
// node-wide gatherer so /v1/metrics carries them alongside the
// existing process / api / chain metric families.
securityMetricsReg, err := metric.MakeAndRegister(
n.MetricsGatherer,
"security",
)
if err != nil {
return fmt.Errorf("security: register metrics namespace: %w", err)
}
securityMetrics := luxsecurity.NewMetrics(metric.NewWithRegistry("security", securityMetricsReg))
securityMetrics.SetActiveProfile(n.securityProfile)
handler, err := luxsecurity.NewHandler(n.Log, n.securityProfile)
if err != nil {
return err
}
return n.APIServer.AddRoute(
handler,
"security",
"",
)
}
// initKeystoreAPI initializes the Keystore API service
func (n *Node) initKeystoreAPI() error {
if !n.Config.KeystoreAPIEnabled {
n.Log.Info("skipping keystore API initialization because it has been disabled")
return nil
}
n.Log.Info("initializing keystore API")
keystoreDB := prefixdb.New([]byte("keystore"), n.DB)
n.keystore = keystore.New(n.Log, keystoreDB)
handler, err := n.keystore.CreateHandler()
if err != nil {
return err
}
return n.APIServer.AddRoute(
handler,
"keystore",
"",
)
}
// initHealthAPI initializes the Health API service
// Assumes n.Log, n.Net, n.APIServer, n.HTTPLog already initialized
func (n *Node) initHealthAPI() error {
healthReg, err := metric.MakeAndRegister(
n.MetricsGatherer,
healthNamespace,
)
if err != nil {
return err
}
n.health, err = health.New(n.Log, healthReg)
if err != nil {
return err
}
if !n.Config.HealthAPIEnabled {
n.Log.Info("skipping health API initialization because it has been disabled")
return nil
}
n.Log.Info("initializing Health API")
err = n.health.RegisterHealthCheck("network", n.Net, health.ApplicationTag)
if err != nil {
return fmt.Errorf("couldn't register network health check: %w", err)
}
// Enable router health check - chainRouter now implements HealthCheck
err = n.health.RegisterHealthCheck("router", n.chainRouter, health.ApplicationTag)
if err != nil {
return fmt.Errorf("couldn't register router health check: %w", err)
}
err = n.health.RegisterHealthCheck("database", n.DB, health.ApplicationTag)
if err != nil {
return fmt.Errorf("couldn't register database health check: %w", err)
}
diskSpaceCheck := health.CheckerFunc(func(context.Context) (interface{}, error) {
availableDiskBytes, err := diskFreeBytes(n.Config.DatabaseConfig.Path)
if err != nil {
return map[string]interface{}{
"availableDiskBytes": uint64(0),
"error": err.Error(),
}, nil
}
return map[string]interface{}{
"availableDiskBytes": availableDiskBytes,
}, nil
})
err = n.health.RegisterHealthCheck("diskspace", diskSpaceCheck, health.ApplicationTag)
if err != nil {
return fmt.Errorf("couldn't register resource health check: %w", err)
}
wrongBLSKeyCheck := health.CheckerFunc(func(context.Context) (interface{}, error) {
vdr, ok := n.vdrs.GetValidator(constants.PrimaryNetworkID, n.ID)
if !ok {
return "node is not a validator", nil
}
vdrPK := vdr.PublicKey
if vdrPK == nil {
return "validator doesn't have a BLS key", nil
}
nodePK := n.Config.StakingSigningKey.PublicKey()
// Validator's public key is stored in uncompressed format (96 bytes),
// so we compare using uncompressed bytes
nodePKBytes := bls.PublicKeyToUncompressedBytes(nodePK)
if bytes.Equal(nodePKBytes, vdrPK) {
return "node has the correct BLS key", nil
}
return nil, fmt.Errorf("node has BLS key 0x%x, but is registered to the validator set with 0x%x",
nodePKBytes,
vdrPK,
)
})
err = n.health.RegisterHealthCheck("bls", wrongBLSKeyCheck, health.ApplicationTag)
if err != nil {
return fmt.Errorf("couldn't register bls health check: %w", err)
}
handler, err := health.NewGetAndPostHandler(n.Log, n.health)
if err != nil {
return err
}
err = n.APIServer.AddRoute(
handler,
"health",
"",
)
if err != nil {
return err
}
err = n.APIServer.AddRoute(
health.NewGetHandler(n.health.Readiness),
"health",
"/readiness",
)
if err != nil {
return err
}
err = n.APIServer.AddRoute(
health.NewGetHandler(n.health.Health),
"health",
"/health",
)
if err != nil {
return err
}
return n.APIServer.AddRoute(
health.NewGetHandler(n.health.Liveness),
"health",
"/liveness",
)
}
// Give chains aliases as specified by the genesis information
func (n *Node) initChainAliases(genesisBytes []byte) error {
n.Log.Info("initializing chain aliases")
_, chainAliases, err := builder.Aliases(genesisBytes)
if err != nil {
return err
}
for chainID, aliases := range chainAliases {
for _, alias := range aliases {
if err := n.chainManager.Alias(chainID, alias); err != nil {
return err
}
}
}
for chainID, aliases := range n.Config.ChainAliases {
for _, alias := range aliases {
if err := n.chainManager.Alias(chainID, alias); err != nil {
return err
}
}
}
return nil
}
// APIs aliases as specified by the genesis information
func (n *Node) initAPIAliases(genesisBytes []byte) error {
n.Log.Info("initializing API aliases")
apiAliases, _, err := builder.Aliases(genesisBytes)
if err != nil {
return err
}
for url, aliases := range apiAliases {
if err := n.APIServer.AddAliases(url, aliases...); err != nil {
return err
}
}
return nil
}
// Initialize [n.resourceManager].
func (n *Node) initResourceManager() error {
systemResourcesRegisterer, err := metric.MakeAndRegister(
n.MetricsGatherer,
systemResourcesNamespace,
)
if err != nil {
return err
}
resourceManager, err := resource.NewManager(
n.Log,
n.Config.DatabaseConfig.Path,
n.Config.SystemTrackerFrequency,
n.Config.SystemTrackerCPUHalflife,
n.Config.SystemTrackerDiskHalflife,
systemResourcesRegisterer,
)
if err != nil {
return err
}
n.resourceManager = resourceManager
n.resourceManager.TrackProcess(os.Getpid())
_, err = metric.MakeAndRegister(
n.MetricsGatherer,
resourceTrackerNamespace,
)
if err != nil {
return err
}
// Create resource tracker with wrapped resource manager
wrappedManager := NewResourceManagerWrapper(n.resourceManager)
n.resourceTracker, err = tracker.NewResourceTracker(
wrappedManager,
n.Config.SystemTrackerFrequency,
)
if err != nil {
return err
}
return nil
}
// Initialize [n.cpuTargeter].
// Assumes [n.resourceTracker] is already initialized.
func (n *Node) initCPUTargeter(
config *tracker.TargeterConfig,
) {
// Create CPU targeter
n.cpuTargeter = tracker.NewTargeter(config)
}
// Initialize [n.diskTargeter].
// Assumes [n.resourceTracker] is already initialized.
func (n *Node) initDiskTargeter(
config *tracker.TargeterConfig,
) {
// Create disk targeter
n.diskTargeter = tracker.NewTargeter(config)
}
// Shutdown this node
// May be called multiple times
// All calls to shutdownOnce.Do block until the first call returns
func (n *Node) Shutdown(exitCode int) {
if !n.shuttingDown.Swap(true) { // only set the exit code once
n.shuttingDownExitCode.Set(exitCode)
}
n.shutdownOnce.Do(n.shutdown)
}
func (n *Node) shutdown() {
n.Log.Info("shutting down node",
"exitCode", n.ExitCode(),
)
if n.health != nil {
// Passes if the node is not shutting down
shuttingDownCheck := health.CheckerFunc(func(context.Context) (interface{}, error) {
return map[string]interface{}{
"isShuttingDown": true,
}, errShuttingDown
})
err := n.health.RegisterHealthCheck("shuttingDown", shuttingDownCheck, health.ApplicationTag)
if err != nil {
n.Log.Debug("couldn't register shuttingDown health check",
"error", err,
)
}
time.Sleep(n.Config.ShutdownWait)
}
if n.resourceManager != nil {
n.resourceManager.Shutdown()
}
if n.chainManager != nil {
n.chainManager.Shutdown()
}
if n.profiler != nil {
n.profiler.Shutdown()
}
if n.Net != nil {
n.Net.StartClose()
}
if err := n.APIServer.Shutdown(); err != nil {
n.Log.Debug("error during API shutdown",
"error", err,
)
}
n.portMapper.UnmapAllPorts()
n.ipUpdater.Stop()
if err := n.indexer.Close(); err != nil {
n.Log.Debug("error closing tx indexer",
"error", err,
)
}
// Ensure all runtimes are shutdown
n.Log.Info("cleaning up plugin runtimes")
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
n.runtimeManager.Stop(shutdownCtx)
if n.DB != nil {
if err := n.DB.Delete(ungracefulShutdown); err != nil {
n.Log.Error(
"failed to delete ungraceful shutdown key",
"error", err,
)
}
if err := n.DB.Close(); err != nil {
n.Log.Warn("error during DB shutdown",
"error", err,
)
}
}
if n.Config.TraceConfig.ExporterConfig.Type != trace.Disabled {
n.Log.Info("shutting down tracing")
}
if err := n.tracer.Close(); err != nil {
n.Log.Warn("error during tracer shutdown",
"error", err,
)
}
n.Log.Info("finished node shutdown")
}
func (n *Node) ExitCode() int {
return n.shuttingDownExitCode.Get()
}
// Real implementations are now in network/tracker/ directory