mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
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>
2029 lines
77 KiB
Go
2029 lines
77 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-json-experiment/json"
|
|
"github.com/go-json-experiment/json/jsontext"
|
|
"github.com/spf13/viper"
|
|
|
|
compression "github.com/luxfi/compress"
|
|
consensusconfig "github.com/luxfi/consensus/config"
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/crypto/bls"
|
|
"github.com/luxfi/crypto/bls/signer/localsigner"
|
|
"github.com/luxfi/crypto/mldsa"
|
|
mlkemcrypto "github.com/luxfi/crypto/mlkem"
|
|
"github.com/luxfi/filesystem/perms"
|
|
"github.com/luxfi/filesystem/storage"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/math/set"
|
|
"github.com/luxfi/net/endpoints"
|
|
"github.com/luxfi/node/benchlist"
|
|
"github.com/luxfi/node/chains"
|
|
"github.com/luxfi/node/config/node"
|
|
"github.com/luxfi/node/genesis/builder"
|
|
"github.com/luxfi/node/nets"
|
|
"github.com/luxfi/node/network"
|
|
"github.com/luxfi/node/network/dialer"
|
|
"github.com/luxfi/node/network/throttling"
|
|
"github.com/luxfi/node/server/http"
|
|
"github.com/luxfi/node/staking"
|
|
"github.com/luxfi/node/trace"
|
|
"github.com/luxfi/node/upgrade"
|
|
"github.com/luxfi/node/utils/profiler"
|
|
"github.com/luxfi/node/version"
|
|
"github.com/luxfi/node/vms/components/gas"
|
|
"github.com/luxfi/node/vms/platformvm/reward"
|
|
"github.com/luxfi/node/vms/platformvm/validators/fee"
|
|
"github.com/luxfi/node/vms/proposervm"
|
|
"github.com/luxfi/timer"
|
|
)
|
|
|
|
// TrackerTargeterConfig contains resource allocation configurations
|
|
type TrackerTargeterConfig struct {
|
|
VdrAlloc float64 `json:"vdrAlloc"`
|
|
MaxNonVdrUsage float64 `json:"maxNonVdrUsage"`
|
|
MaxNonVdrNodeUsage float64 `json:"maxNonVdrNodeUsage"`
|
|
}
|
|
|
|
const (
|
|
chainConfigFileName = "config"
|
|
chainUpgradeFileName = "upgrade"
|
|
chainConfigFileExt = ".json"
|
|
)
|
|
|
|
var (
|
|
// Deprecated key --> deprecation message (i.e. which key replaces it)
|
|
deprecatedKeys = map[string]string{
|
|
BootstrapIPsKey: "use --bootstrap-nodes instead",
|
|
BootstrapIDsKey: "use --bootstrap-nodes instead",
|
|
}
|
|
|
|
errConflictingLPOpinion = errors.New("supporting and objecting to the same LP")
|
|
errConflictingImplicitLPOpinion = errors.New("objecting to enabled LP")
|
|
errSybilProtectionDisabledStakerWeights = errors.New("sybil protection disabled weights must be positive")
|
|
errSybilProtectionDisabledOnPublicNetwork = errors.New("sybil protection disabled on public network")
|
|
errInvalidUptimeRequirement = errors.New("uptime requirement must be in the range [0, 1]")
|
|
errMinValidatorStakeAboveMax = errors.New("minimum validator stake can't be greater than maximum validator stake")
|
|
errInvalidDelegationFee = errors.New("delegation fee must be in the range [0, 1,000,000]")
|
|
errInvalidMinStakeDuration = errors.New("min stake duration must be > 0")
|
|
errMinStakeDurationAboveMax = errors.New("max stake duration can't be less than min stake duration")
|
|
errStakeMaxConsumptionTooLarge = fmt.Errorf("max stake consumption must be less than or equal to %d", reward.PercentDenominator)
|
|
errStakeMaxConsumptionBelowMin = errors.New("stake max consumption can't be less than min stake consumption")
|
|
errStakeMintingPeriodBelowMin = errors.New("stake minting period can't be less than max stake duration")
|
|
errCannotTrackPrimaryNetwork = errors.New("cannot track primary network")
|
|
errStakingKeyContentUnset = fmt.Errorf("%s key not set but %s set", StakingTLSKeyContentKey, StakingCertContentKey)
|
|
errStakingCertContentUnset = fmt.Errorf("%s key set but %s not set", StakingTLSKeyContentKey, StakingCertContentKey)
|
|
errMissingStakingSigningKeyFile = errors.New("missing staking signing key file")
|
|
errPluginDirNotADirectory = errors.New("plugin dir is not a directory")
|
|
errCannotReadDirectory = errors.New("cannot read directory")
|
|
errUnmarshalling = errors.New("unmarshalling failed")
|
|
errFileDoesNotExist = errors.New("file does not exist")
|
|
)
|
|
|
|
// (Removed: AutomineNetworkConfig + loadAutomineNetworkConfig +
|
|
// saveAutomineNetworkConfig + automine-network.json persistence.)
|
|
//
|
|
// --automine is a *consensus-mode* flag (single-node, single-validator
|
|
// quorum), nothing more. It MUST NOT swap in a different C-Chain
|
|
// genesis or persist any "dev network config" sidecar. C-Chain
|
|
// genesis is canonical genesis — for network-id 1/2/3/1337 it's the
|
|
// embedded luxfi/genesis config, period. If the operator wants a
|
|
// custom genesis they pass --genesis-file. No backwards compatibility
|
|
// for the deleted automine-network.json sidecar.
|
|
|
|
func getConsensusConfig(v *viper.Viper) consensusconfig.Parameters {
|
|
// Start with default parameters
|
|
p := consensusconfig.DefaultParams()
|
|
|
|
// Override with config values if set
|
|
if v.IsSet(ConsensusSampleSizeKey) {
|
|
p.K = v.GetInt(ConsensusSampleSizeKey)
|
|
}
|
|
if v.IsSet(ConsensusPreferenceQuorumSizeKey) {
|
|
p.AlphaPreference = v.GetInt(ConsensusPreferenceQuorumSizeKey)
|
|
}
|
|
if v.IsSet(ConsensusConfidenceQuorumSizeKey) {
|
|
p.AlphaConfidence = v.GetInt(ConsensusConfidenceQuorumSizeKey)
|
|
}
|
|
if v.IsSet(ConsensusCommitThresholdKey) {
|
|
p.Beta = uint32(v.GetInt(ConsensusCommitThresholdKey))
|
|
}
|
|
if v.IsSet(ConsensusConcurrentRepollsKey) {
|
|
p.ConcurrentRepolls = v.GetInt(ConsensusConcurrentRepollsKey)
|
|
}
|
|
if v.IsSet(ConsensusOptimalProcessingKey) {
|
|
p.OptimalProcessing = v.GetInt(ConsensusOptimalProcessingKey)
|
|
}
|
|
if v.IsSet(ConsensusMaxProcessingKey) {
|
|
p.MaxOutstandingItems = v.GetInt(ConsensusMaxProcessingKey)
|
|
}
|
|
if v.IsSet(ConsensusMaxTimeProcessingKey) {
|
|
p.MaxItemProcessingTime = v.GetDuration(ConsensusMaxTimeProcessingKey)
|
|
}
|
|
if v.IsSet(ConsensusQuorumSizeKey) {
|
|
p.AlphaPreference = v.GetInt(ConsensusQuorumSizeKey)
|
|
p.AlphaConfidence = p.AlphaPreference
|
|
}
|
|
return p
|
|
}
|
|
|
|
func getHTTPConfig(v *viper.Viper) (node.HTTPConfig, error) {
|
|
var (
|
|
httpsKey []byte
|
|
httpsCert []byte
|
|
err error
|
|
)
|
|
switch {
|
|
case v.IsSet(HTTPSKeyContentKey):
|
|
rawContent := v.GetString(HTTPSKeyContentKey)
|
|
httpsKey, err = base64.StdEncoding.DecodeString(rawContent)
|
|
if err != nil {
|
|
return node.HTTPConfig{}, fmt.Errorf("unable to decode base64 content: %w", err)
|
|
}
|
|
case v.IsSet(HTTPSKeyFileKey):
|
|
httpsKeyFilepath := getExpandedArg(v, HTTPSKeyFileKey)
|
|
httpsKey, err = os.ReadFile(filepath.Clean(httpsKeyFilepath))
|
|
if err != nil {
|
|
return node.HTTPConfig{}, err
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case v.IsSet(HTTPSCertContentKey):
|
|
rawContent := v.GetString(HTTPSCertContentKey)
|
|
httpsCert, err = base64.StdEncoding.DecodeString(rawContent)
|
|
if err != nil {
|
|
return node.HTTPConfig{}, fmt.Errorf("unable to decode base64 content: %w", err)
|
|
}
|
|
case v.IsSet(HTTPSCertFileKey):
|
|
httpsCertFilepath := getExpandedArg(v, HTTPSCertFileKey)
|
|
httpsCert, err = os.ReadFile(filepath.Clean(httpsCertFilepath))
|
|
if err != nil {
|
|
return node.HTTPConfig{}, err
|
|
}
|
|
}
|
|
|
|
return node.HTTPConfig{
|
|
HTTPConfig: server.HTTPConfig{
|
|
ReadTimeout: v.GetDuration(HTTPReadTimeoutKey),
|
|
ReadHeaderTimeout: v.GetDuration(HTTPReadHeaderTimeoutKey),
|
|
WriteTimeout: v.GetDuration(HTTPWriteTimeoutKey),
|
|
IdleTimeout: v.GetDuration(HTTPIdleTimeoutKey),
|
|
},
|
|
APIConfig: node.APIConfig{
|
|
APIIndexerConfig: node.APIIndexerConfig{
|
|
IndexAPIEnabled: v.GetBool(IndexEnabledKey),
|
|
IndexAllowIncomplete: v.GetBool(IndexAllowIncompleteKey),
|
|
},
|
|
AdminAPIEnabled: v.GetBool(AdminAPIEnabledKey),
|
|
InfoAPIEnabled: v.GetBool(InfoAPIEnabledKey),
|
|
KeystoreAPIEnabled: v.GetBool(KeystoreAPIEnabledKey),
|
|
MetricsAPIEnabled: v.GetBool(MetricsAPIEnabledKey),
|
|
HealthAPIEnabled: v.GetBool(HealthAPIEnabledKey),
|
|
},
|
|
HTTPHost: v.GetString(HTTPHostKey),
|
|
HTTPPort: uint16(v.GetUint(HTTPPortKey)),
|
|
HTTPSEnabled: v.GetBool(HTTPSEnabledKey),
|
|
HTTPSKey: httpsKey,
|
|
HTTPSCert: httpsCert,
|
|
HTTPAllowedOrigins: v.GetStringSlice(HTTPAllowedOrigins),
|
|
HTTPAllowedHosts: v.GetStringSlice(HTTPAllowedHostsKey),
|
|
ShutdownTimeout: v.GetDuration(HTTPShutdownTimeoutKey),
|
|
ShutdownWait: v.GetDuration(HTTPShutdownWaitKey),
|
|
}, nil
|
|
}
|
|
|
|
func getAdaptiveTimeoutConfig(v *viper.Viper) (timer.AdaptiveTimeoutConfig, error) {
|
|
config := timer.AdaptiveTimeoutConfig{
|
|
InitialTimeout: v.GetDuration(NetworkInitialTimeoutKey),
|
|
MinimumTimeout: v.GetDuration(NetworkMinimumTimeoutKey),
|
|
MaximumTimeout: v.GetDuration(NetworkMaximumTimeoutKey),
|
|
TimeoutHalflife: v.GetDuration(NetworkTimeoutHalflifeKey),
|
|
TimeoutCoefficient: v.GetFloat64(NetworkTimeoutCoefficientKey),
|
|
}
|
|
switch {
|
|
case config.MinimumTimeout < 1:
|
|
return timer.AdaptiveTimeoutConfig{}, fmt.Errorf("%q must be positive", NetworkMinimumTimeoutKey)
|
|
case config.MinimumTimeout > config.MaximumTimeout:
|
|
return timer.AdaptiveTimeoutConfig{}, fmt.Errorf("%q must be >= %q", NetworkMaximumTimeoutKey, NetworkMinimumTimeoutKey)
|
|
case config.InitialTimeout < config.MinimumTimeout || config.InitialTimeout > config.MaximumTimeout:
|
|
return timer.AdaptiveTimeoutConfig{}, fmt.Errorf("%q must be in [%q, %q]", NetworkInitialTimeoutKey, NetworkMinimumTimeoutKey, NetworkMaximumTimeoutKey)
|
|
case config.TimeoutHalflife <= 0:
|
|
return timer.AdaptiveTimeoutConfig{}, fmt.Errorf("%q must > 0", NetworkTimeoutHalflifeKey)
|
|
case config.TimeoutCoefficient < 1:
|
|
return timer.AdaptiveTimeoutConfig{}, fmt.Errorf("%q must be >= 1", NetworkTimeoutCoefficientKey)
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
func getNetworkConfig(
|
|
v *viper.Viper,
|
|
networkID uint32,
|
|
sybilProtectionEnabled bool,
|
|
halflife time.Duration,
|
|
) (network.Config, error) {
|
|
// Set the max number of recent inbound connections upgraded to be
|
|
// equal to the max number of inbound connections per second.
|
|
maxInboundConnsPerSec := v.GetFloat64(NetworkInboundThrottlerMaxConnsPerSecKey)
|
|
upgradeCooldown := v.GetDuration(NetworkInboundConnUpgradeThrottlerCooldownKey)
|
|
upgradeCooldownInSeconds := upgradeCooldown.Seconds()
|
|
maxRecentConnsUpgraded := int(math.Ceil(maxInboundConnsPerSec * upgradeCooldownInSeconds))
|
|
|
|
compressionType, err := compression.TypeFromString(v.GetString(NetworkCompressionTypeKey))
|
|
if err != nil {
|
|
return network.Config{}, err
|
|
}
|
|
|
|
// Allow private IPs by default for local development and testing.
|
|
// Use --network-allow-private-ips=false to prohibit for production deployments.
|
|
allowPrivateIPs := true
|
|
if v.IsSet(NetworkAllowPrivateIPsKey) {
|
|
allowPrivateIPs = v.GetBool(NetworkAllowPrivateIPsKey)
|
|
}
|
|
|
|
var supportedLPs set.Set[uint32]
|
|
for _, lp := range v.GetIntSlice(LPSupportKey) {
|
|
if lp < 0 || lp > math.MaxInt32 {
|
|
return network.Config{}, fmt.Errorf("invalid LP: %d", lp)
|
|
}
|
|
supportedLPs.Add(uint32(lp))
|
|
}
|
|
|
|
var objectedLPs set.Set[uint32]
|
|
for _, lp := range v.GetIntSlice(LPObjectKey) {
|
|
if lp < 0 || lp > math.MaxInt32 {
|
|
return network.Config{}, fmt.Errorf("invalid LP: %d", lp)
|
|
}
|
|
objectedLPs.Add(uint32(lp))
|
|
}
|
|
if supportedLPs.Overlaps(objectedLPs) {
|
|
return network.Config{}, errConflictingLPOpinion
|
|
}
|
|
if constants.ScheduledLPs.Overlaps(objectedLPs) {
|
|
return network.Config{}, errConflictingImplicitLPOpinion
|
|
}
|
|
|
|
// Because this node version has scheduled these LPs, we should notify
|
|
// peers that we support these upgrades.
|
|
supportedLPs.Union(constants.ScheduledLPs)
|
|
|
|
// To decrease unnecessary network traffic, peers will not be notified of
|
|
// objection or support of activated LPs.
|
|
supportedLPs.Difference(constants.ActivatedLPs)
|
|
objectedLPs.Difference(constants.ActivatedLPs)
|
|
|
|
config := network.Config{
|
|
ThrottlerConfig: network.ThrottlerConfig{
|
|
MaxInboundConnsPerSec: maxInboundConnsPerSec,
|
|
InboundConnUpgradeThrottlerConfig: throttling.InboundConnUpgradeThrottlerConfig{
|
|
UpgradeCooldown: upgradeCooldown,
|
|
MaxRecentConnsUpgraded: maxRecentConnsUpgraded,
|
|
},
|
|
|
|
InboundMsgThrottlerConfig: throttling.InboundMsgThrottlerConfig{
|
|
MsgByteThrottlerConfig: throttling.MsgByteThrottlerConfig{
|
|
AtLargeAllocSize: v.GetUint64(InboundThrottlerAtLargeAllocSizeKey),
|
|
VdrAllocSize: v.GetUint64(InboundThrottlerVdrAllocSizeKey),
|
|
NodeMaxAtLargeBytes: v.GetUint64(InboundThrottlerNodeMaxAtLargeBytesKey),
|
|
},
|
|
BandwidthThrottlerConfig: throttling.BandwidthThrottlerConfig{
|
|
RefillRate: v.GetUint64(InboundThrottlerBandwidthRefillRateKey),
|
|
MaxBurstSize: v.GetUint64(InboundThrottlerBandwidthMaxBurstSizeKey),
|
|
},
|
|
MaxProcessingMsgsPerNode: v.GetUint64(InboundThrottlerMaxProcessingMsgsPerNodeKey),
|
|
CPUThrottlerConfig: throttling.SystemThrottlerConfig{
|
|
MaxRecheckDelay: v.GetDuration(InboundThrottlerCPUMaxRecheckDelayKey),
|
|
},
|
|
DiskThrottlerConfig: throttling.SystemThrottlerConfig{
|
|
MaxRecheckDelay: v.GetDuration(InboundThrottlerDiskMaxRecheckDelayKey),
|
|
},
|
|
},
|
|
|
|
OutboundMsgThrottlerConfig: throttling.MsgByteThrottlerConfig{
|
|
AtLargeAllocSize: v.GetUint64(OutboundThrottlerAtLargeAllocSizeKey),
|
|
VdrAllocSize: v.GetUint64(OutboundThrottlerVdrAllocSizeKey),
|
|
NodeMaxAtLargeBytes: v.GetUint64(OutboundThrottlerNodeMaxAtLargeBytesKey),
|
|
},
|
|
},
|
|
|
|
HealthConfig: network.HealthConfig{
|
|
Enabled: sybilProtectionEnabled,
|
|
MaxTimeSinceMsgSent: v.GetDuration(NetworkHealthMaxTimeSinceMsgSentKey),
|
|
MaxTimeSinceMsgReceived: v.GetDuration(NetworkHealthMaxTimeSinceMsgReceivedKey),
|
|
MaxPortionSendQueueBytesFull: v.GetFloat64(NetworkHealthMaxPortionSendQueueFillKey),
|
|
MinConnectedPeers: v.GetUint(NetworkHealthMinPeersKey),
|
|
MaxSendFailRate: v.GetFloat64(NetworkHealthMaxSendFailRateKey),
|
|
SendFailRateHalflife: halflife,
|
|
NoIngressValidatorConnectionGracePeriod: v.GetDuration(NetworkNoIngressValidatorConnectionsGracePeriodKey),
|
|
},
|
|
|
|
ProxyEnabled: v.GetBool(NetworkTCPProxyEnabledKey),
|
|
ProxyReadHeaderTimeout: v.GetDuration(NetworkTCPProxyReadTimeoutKey),
|
|
|
|
DialerConfig: dialer.Config{
|
|
ThrottleRps: v.GetUint32(NetworkOutboundConnectionThrottlingRpsKey),
|
|
ConnectionTimeout: v.GetDuration(NetworkOutboundConnectionTimeoutKey),
|
|
},
|
|
|
|
TLSKeyLogFile: v.GetString(NetworkTLSKeyLogFileKey),
|
|
|
|
TimeoutConfig: network.TimeoutConfig{
|
|
PingPongTimeout: v.GetDuration(NetworkPingTimeoutKey),
|
|
ReadHandshakeTimeout: v.GetDuration(NetworkReadHandshakeTimeoutKey),
|
|
},
|
|
|
|
PeerListGossipConfig: network.PeerListGossipConfig{
|
|
PeerListNumValidatorIPs: v.GetUint32(NetworkPeerListNumValidatorIPsKey),
|
|
PeerListPullGossipFreq: v.GetDuration(NetworkPeerListPullGossipFreqKey),
|
|
PeerListBloomResetFreq: v.GetDuration(NetworkPeerListBloomResetFreqKey),
|
|
},
|
|
|
|
DelayConfig: network.DelayConfig{
|
|
MaxReconnectDelay: v.GetDuration(NetworkMaxReconnectDelayKey),
|
|
InitialReconnectDelay: v.GetDuration(NetworkInitialReconnectDelayKey),
|
|
},
|
|
|
|
MaxClockDifference: v.GetDuration(NetworkMaxClockDifferenceKey),
|
|
CompressionType: compressionType,
|
|
PingFrequency: v.GetDuration(NetworkPingFrequencyKey),
|
|
AllowPrivateIPs: allowPrivateIPs,
|
|
UptimeMetricFreq: v.GetDuration(UptimeMetricFreqKey),
|
|
MaximumInboundMessageTimeout: v.GetDuration(NetworkMaximumInboundTimeoutKey),
|
|
|
|
SupportedLPs: supportedLPs,
|
|
ObjectedLPs: objectedLPs,
|
|
|
|
RequireValidatorToConnect: v.GetBool(NetworkRequireValidatorToConnectKey),
|
|
PeerReadBufferSize: int(v.GetUint(NetworkPeerReadBufferSizeKey)),
|
|
PeerWriteBufferSize: int(v.GetUint(NetworkPeerWriteBufferSizeKey)),
|
|
}
|
|
|
|
switch {
|
|
case config.HealthConfig.MaxTimeSinceMsgSent < 0:
|
|
return network.Config{}, fmt.Errorf("%s must be >= 0", NetworkHealthMaxTimeSinceMsgSentKey)
|
|
case config.HealthConfig.MaxTimeSinceMsgReceived < 0:
|
|
return network.Config{}, fmt.Errorf("%s must be >= 0", NetworkHealthMaxTimeSinceMsgReceivedKey)
|
|
case config.HealthConfig.MaxSendFailRate < 0 || config.HealthConfig.MaxSendFailRate > 1:
|
|
return network.Config{}, fmt.Errorf("%s must be in [0,1]", NetworkHealthMaxSendFailRateKey)
|
|
case config.HealthConfig.MaxPortionSendQueueBytesFull < 0 || config.HealthConfig.MaxPortionSendQueueBytesFull > 1:
|
|
return network.Config{}, fmt.Errorf("%s must be in [0,1]", NetworkHealthMaxPortionSendQueueFillKey)
|
|
case config.DialerConfig.ConnectionTimeout < 0:
|
|
return network.Config{}, fmt.Errorf("%q must be >= 0", NetworkOutboundConnectionTimeoutKey)
|
|
case config.PeerListPullGossipFreq < 0:
|
|
return network.Config{}, fmt.Errorf("%s must be >= 0", NetworkPeerListPullGossipFreqKey)
|
|
case config.PeerListBloomResetFreq < 0:
|
|
return network.Config{}, fmt.Errorf("%s must be >= 0", NetworkPeerListBloomResetFreqKey)
|
|
case config.ThrottlerConfig.InboundMsgThrottlerConfig.CPUThrottlerConfig.MaxRecheckDelay < constants.MinInboundThrottlerMaxRecheckDelay:
|
|
return network.Config{}, fmt.Errorf("%s must be >= %d", InboundThrottlerCPUMaxRecheckDelayKey, constants.MinInboundThrottlerMaxRecheckDelay)
|
|
case config.ThrottlerConfig.InboundMsgThrottlerConfig.DiskThrottlerConfig.MaxRecheckDelay < constants.MinInboundThrottlerMaxRecheckDelay:
|
|
return network.Config{}, fmt.Errorf("%s must be >= %d", InboundThrottlerDiskMaxRecheckDelayKey, constants.MinInboundThrottlerMaxRecheckDelay)
|
|
case config.MaxReconnectDelay < 0:
|
|
return network.Config{}, fmt.Errorf("%s must be >= 0", NetworkMaxReconnectDelayKey)
|
|
case config.InitialReconnectDelay < 0:
|
|
return network.Config{}, fmt.Errorf("%s must be >= 0", NetworkInitialReconnectDelayKey)
|
|
case config.MaxReconnectDelay < config.InitialReconnectDelay:
|
|
return network.Config{}, fmt.Errorf("%s must be >= %s", NetworkMaxReconnectDelayKey, NetworkInitialReconnectDelayKey)
|
|
case config.PingPongTimeout < 0:
|
|
return network.Config{}, fmt.Errorf("%s must be >= 0", NetworkPingTimeoutKey)
|
|
case config.PingFrequency < 0:
|
|
return network.Config{}, fmt.Errorf("%s must be >= 0", NetworkPingFrequencyKey)
|
|
case config.PingPongTimeout <= config.PingFrequency:
|
|
return network.Config{}, fmt.Errorf("%s must be > %s", NetworkPingTimeoutKey, NetworkPingFrequencyKey)
|
|
case config.ReadHandshakeTimeout < 0:
|
|
return network.Config{}, fmt.Errorf("%s must be >= 0", NetworkReadHandshakeTimeoutKey)
|
|
case config.MaxClockDifference < 0:
|
|
return network.Config{}, fmt.Errorf("%s must be >= 0", NetworkMaxClockDifferenceKey)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func getStateSyncConfig(v *viper.Viper) (node.StateSyncConfig, error) {
|
|
var (
|
|
config = node.StateSyncConfig{}
|
|
stateSyncIPs = strings.Split(v.GetString(StateSyncIPsKey), ",")
|
|
stateSyncIDs = strings.Split(v.GetString(StateSyncIDsKey), ",")
|
|
)
|
|
|
|
for _, ip := range stateSyncIPs {
|
|
if ip == "" {
|
|
continue
|
|
}
|
|
addr, err := endpoints.ParseAddrPort(ip)
|
|
if err != nil {
|
|
return node.StateSyncConfig{}, fmt.Errorf("couldn't parse state sync ip %s: %w", ip, err)
|
|
}
|
|
config.StateSyncIPs = append(config.StateSyncIPs, addr)
|
|
}
|
|
|
|
for _, id := range stateSyncIDs {
|
|
if id == "" {
|
|
continue
|
|
}
|
|
nodeID, err := ids.NodeIDFromString(id)
|
|
if err != nil {
|
|
return node.StateSyncConfig{}, fmt.Errorf("couldn't parse state sync peer id %s: %w", id, err)
|
|
}
|
|
config.StateSyncIDs = append(config.StateSyncIDs, nodeID)
|
|
}
|
|
|
|
lenIPs := len(config.StateSyncIPs)
|
|
lenIDs := len(config.StateSyncIDs)
|
|
if lenIPs != lenIDs {
|
|
return node.StateSyncConfig{}, fmt.Errorf("expected the number of stateSyncIPs (%d) to match the number of stateSyncIDs (%d)", lenIPs, lenIDs)
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
func getBootstrapConfig(v *viper.Viper, networkID uint32) (node.BootstrapConfig, error) {
|
|
config := node.BootstrapConfig{
|
|
BootstrapBeaconConnectionTimeout: v.GetDuration(BootstrapBeaconConnectionTimeoutKey),
|
|
BootstrapMaxTimeGetAncestors: v.GetDuration(BootstrapMaxTimeGetAncestorsKey),
|
|
BootstrapAncestorsMaxContainersSent: int(v.GetUint(BootstrapAncestorsMaxContainersSentKey)),
|
|
BootstrapAncestorsMaxContainersReceived: int(v.GetUint(BootstrapAncestorsMaxContainersReceivedKey)),
|
|
SkipBootstrap: v.GetBool(SkipBootstrapKey),
|
|
EnableAutomining: v.GetBool(EnableAutominingKey),
|
|
}
|
|
|
|
nodesSet := v.IsSet(BootstrapNodesKey)
|
|
ipsSet := v.IsSet(BootstrapIPsKey)
|
|
idsSet := v.IsSet(BootstrapIDsKey)
|
|
|
|
// --bootstrap-nodes takes precedence: just endpoints, NodeID discovered
|
|
// from the peer's staking certificate during TLS handshake.
|
|
if nodesSet {
|
|
if ipsSet || idsSet {
|
|
return node.BootstrapConfig{}, fmt.Errorf("%q cannot be combined with deprecated %q/%q flags", BootstrapNodesKey, BootstrapIPsKey, BootstrapIDsKey)
|
|
}
|
|
nodes := strings.Split(v.GetString(BootstrapNodesKey), ",")
|
|
config.Bootstrappers = make([]builder.Bootstrapper, 0, len(nodes))
|
|
for _, n := range nodes {
|
|
endpointStr := strings.TrimSpace(n)
|
|
if endpointStr == "" {
|
|
continue
|
|
}
|
|
endpoint, err := endpoints.ParseEndpoint(endpointStr)
|
|
if err != nil {
|
|
return node.BootstrapConfig{}, fmt.Errorf("couldn't parse bootstrap node endpoint %s: %w", endpointStr, err)
|
|
}
|
|
// ID left as zero value — discovered from peer cert during handshake
|
|
config.Bootstrappers = append(config.Bootstrappers, builder.Bootstrapper{
|
|
Endpoint: endpoint,
|
|
})
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
// Legacy: --bootstrap-ips + --bootstrap-ids (deprecated)
|
|
if ipsSet && !idsSet {
|
|
return node.BootstrapConfig{}, fmt.Errorf("set %q but didn't set %q", BootstrapIPsKey, BootstrapIDsKey)
|
|
}
|
|
if !ipsSet && idsSet {
|
|
return node.BootstrapConfig{}, fmt.Errorf("set %q but didn't set %q", BootstrapIDsKey, BootstrapIPsKey)
|
|
}
|
|
if !ipsSet && !idsSet {
|
|
var err error
|
|
config.Bootstrappers, err = builder.SampleBootstrappers(networkID, 5)
|
|
if err != nil {
|
|
return node.BootstrapConfig{}, fmt.Errorf("failed to sample bootstrappers: %w", err)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
bootstrapIPs := strings.Split(v.GetString(BootstrapIPsKey), ",")
|
|
config.Bootstrappers = make([]builder.Bootstrapper, 0, len(bootstrapIPs))
|
|
for _, bootstrapIP := range bootstrapIPs {
|
|
endpointStr := strings.TrimSpace(bootstrapIP)
|
|
if endpointStr == "" {
|
|
continue
|
|
}
|
|
endpoint, err := endpoints.ParseEndpoint(endpointStr)
|
|
if err != nil {
|
|
return node.BootstrapConfig{}, fmt.Errorf("couldn't parse bootstrap endpoint %s: %w", endpointStr, err)
|
|
}
|
|
config.Bootstrappers = append(config.Bootstrappers, builder.Bootstrapper{
|
|
Endpoint: endpoint,
|
|
})
|
|
}
|
|
|
|
bootstrapIDs := strings.Split(v.GetString(BootstrapIDsKey), ",")
|
|
bootstrapNodeIDs := make([]ids.NodeID, 0, len(bootstrapIDs))
|
|
for _, bootstrapID := range bootstrapIDs {
|
|
id := strings.TrimSpace(bootstrapID)
|
|
if id == "" {
|
|
continue
|
|
}
|
|
nodeID, err := ids.NodeIDFromString(id)
|
|
if err != nil {
|
|
return node.BootstrapConfig{}, fmt.Errorf("couldn't parse bootstrap peer id %s: %w", id, err)
|
|
}
|
|
bootstrapNodeIDs = append(bootstrapNodeIDs, nodeID)
|
|
}
|
|
|
|
if len(config.Bootstrappers) != len(bootstrapNodeIDs) {
|
|
return node.BootstrapConfig{}, fmt.Errorf("expected the number of bootstrapIPs (%d) to match the number of bootstrapIDs (%d)", len(config.Bootstrappers), len(bootstrapNodeIDs))
|
|
}
|
|
for i, nodeID := range bootstrapNodeIDs {
|
|
config.Bootstrappers[i].ID = nodeID
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
func getIPConfig(v *viper.Viper) (node.IPConfig, error) {
|
|
ipConfig := node.IPConfig{
|
|
PublicIP: v.GetString(PublicIPKey),
|
|
PublicIPResolutionService: v.GetString(PublicIPResolutionServiceKey),
|
|
PublicIPResolutionFreq: v.GetDuration(PublicIPResolutionFreqKey),
|
|
ListenHost: v.GetString(StakingHostKey),
|
|
ListenPort: uint16(v.GetUint(StakingPortKey)),
|
|
}
|
|
if ipConfig.PublicIPResolutionFreq <= 0 {
|
|
return node.IPConfig{}, fmt.Errorf("%q must be > 0", PublicIPResolutionFreqKey)
|
|
}
|
|
if ipConfig.PublicIP != "" && ipConfig.PublicIPResolutionService != "" {
|
|
return node.IPConfig{}, fmt.Errorf("only one of --%s and --%s can be given", PublicIPKey, PublicIPResolutionServiceKey)
|
|
}
|
|
return ipConfig, nil
|
|
}
|
|
|
|
func getProfilerConfig(v *viper.Viper) (profiler.Config, error) {
|
|
config := profiler.Config{
|
|
Dir: getExpandedArg(v, ProfileDirKey),
|
|
Enabled: v.GetBool(ProfileContinuousEnabledKey),
|
|
Freq: v.GetDuration(ProfileContinuousFreqKey),
|
|
MaxNumFiles: v.GetInt(ProfileContinuousMaxFilesKey),
|
|
}
|
|
if config.Freq < 0 {
|
|
return profiler.Config{}, fmt.Errorf("%s must be >= 0", ProfileContinuousFreqKey)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func getStakingTLSCertFromFlag(v *viper.Viper) (tls.Certificate, error) {
|
|
stakingKeyRawContent := v.GetString(StakingTLSKeyContentKey)
|
|
stakingKeyContent, err := base64.StdEncoding.DecodeString(stakingKeyRawContent)
|
|
if err != nil {
|
|
return tls.Certificate{}, fmt.Errorf("unable to decode base64 content: %w", err)
|
|
}
|
|
|
|
stakingCertRawContent := v.GetString(StakingCertContentKey)
|
|
stakingCertContent, err := base64.StdEncoding.DecodeString(stakingCertRawContent)
|
|
if err != nil {
|
|
return tls.Certificate{}, fmt.Errorf("unable to decode base64 content: %w", err)
|
|
}
|
|
|
|
cert, err := staking.LoadTLSCertFromBytes(stakingKeyContent, stakingCertContent)
|
|
if err != nil {
|
|
return tls.Certificate{}, fmt.Errorf("failed creating cert: %w", err)
|
|
}
|
|
|
|
return *cert, nil
|
|
}
|
|
|
|
func getStakingTLSCertFromFile(v *viper.Viper) (tls.Certificate, error) {
|
|
// Parse the staking key/cert paths and expand environment variables
|
|
stakingKeyPath := getExpandedArg(v, StakingTLSKeyPathKey)
|
|
stakingCertPath := getExpandedArg(v, StakingCertPathKey)
|
|
|
|
// If staking key/cert locations are specified but not found, error
|
|
if v.IsSet(StakingTLSKeyPathKey) || v.IsSet(StakingCertPathKey) {
|
|
if _, err := os.Stat(stakingKeyPath); os.IsNotExist(err) {
|
|
return tls.Certificate{}, fmt.Errorf("couldn't find staking key at %s", stakingKeyPath)
|
|
} else if _, err := os.Stat(stakingCertPath); os.IsNotExist(err) {
|
|
return tls.Certificate{}, fmt.Errorf("couldn't find staking certificate at %s", stakingCertPath)
|
|
}
|
|
} else {
|
|
// Create the staking key/cert if [stakingKeyPath] and [stakingCertPath] don't exist
|
|
if err := staking.InitNodeStakingKeyPair(stakingKeyPath, stakingCertPath); err != nil {
|
|
return tls.Certificate{}, fmt.Errorf("couldn't generate staking key/cert: %w", err)
|
|
}
|
|
}
|
|
|
|
// Load and parse the staking key/cert
|
|
cert, err := staking.LoadTLSCertFromFiles(stakingKeyPath, stakingCertPath)
|
|
if err != nil {
|
|
return tls.Certificate{}, fmt.Errorf("couldn't read staking certificate: %w", err)
|
|
}
|
|
return *cert, nil
|
|
}
|
|
|
|
func getStakingTLSCert(v *viper.Viper) (tls.Certificate, error) {
|
|
if v.GetBool(StakingEphemeralCertEnabledKey) {
|
|
// Use an ephemeral staking key/cert
|
|
cert, err := staking.NewTLSCert()
|
|
if err != nil {
|
|
return tls.Certificate{}, fmt.Errorf("couldn't generate ephemeral staking key/cert: %w", err)
|
|
}
|
|
return *cert, nil
|
|
}
|
|
|
|
switch {
|
|
case v.IsSet(StakingTLSKeyContentKey) && !v.IsSet(StakingCertContentKey):
|
|
return tls.Certificate{}, errStakingCertContentUnset
|
|
case !v.IsSet(StakingTLSKeyContentKey) && v.IsSet(StakingCertContentKey):
|
|
return tls.Certificate{}, errStakingKeyContentUnset
|
|
case v.IsSet(StakingTLSKeyContentKey) && v.IsSet(StakingCertContentKey):
|
|
return getStakingTLSCertFromFlag(v)
|
|
case v.IsSet(StakingKMSEndpointKey):
|
|
keys, err := staking.FetchFromKMS(staking.KMSConfig{
|
|
Endpoint: v.GetString(StakingKMSEndpointKey),
|
|
SecretPath: v.GetString(StakingKMSSecretPathKey),
|
|
AuthToken: v.GetString(StakingKMSTokenKey),
|
|
})
|
|
if err != nil {
|
|
return tls.Certificate{}, fmt.Errorf("fetching staking keys from KMS: %w", err)
|
|
}
|
|
cert, err := staking.LoadTLSCertFromBytes([]byte(keys.TLSKey), []byte(keys.TLSCert))
|
|
if err != nil {
|
|
return tls.Certificate{}, fmt.Errorf("loading TLS cert from KMS: %w", err)
|
|
}
|
|
return *cert, nil
|
|
default:
|
|
cert, err := getStakingTLSCertFromFile(v)
|
|
if err != nil {
|
|
return tls.Certificate{}, err
|
|
}
|
|
// Verify Leaf is populated
|
|
if cert.Leaf == nil {
|
|
return tls.Certificate{}, fmt.Errorf("cert.Leaf is nil after loading from file")
|
|
}
|
|
return cert, nil
|
|
}
|
|
}
|
|
|
|
func getStakingSigner(v *viper.Viper) (bls.Signer, error) {
|
|
if v.GetBool(StakingEphemeralSignerEnabledKey) {
|
|
key, err := localsigner.New()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't generate ephemeral signing key: %w", err)
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
if v.IsSet(StakingSignerKeyContentKey) {
|
|
signerKeyRawContent := v.GetString(StakingSignerKeyContentKey)
|
|
signerKeyContent, err := base64.StdEncoding.DecodeString(signerKeyRawContent)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode base64 content: %w", err)
|
|
}
|
|
key, err := localsigner.FromBytes(signerKeyContent)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't parse signing key: %w", err)
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
if v.IsSet(StakingKMSEndpointKey) {
|
|
keys, err := staking.FetchFromKMS(staking.KMSConfig{
|
|
Endpoint: v.GetString(StakingKMSEndpointKey),
|
|
SecretPath: v.GetString(StakingKMSSecretPathKey),
|
|
AuthToken: v.GetString(StakingKMSTokenKey),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetching signer key from KMS: %w", err)
|
|
}
|
|
if keys.SignerKey != "" {
|
|
signerKeyContent, err := hex.DecodeString(keys.SignerKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decoding hex signer key from KMS: %w", err)
|
|
}
|
|
key, err := localsigner.FromBytes(signerKeyContent)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing signer key from KMS: %w", err)
|
|
}
|
|
return key, nil
|
|
}
|
|
}
|
|
|
|
signingKeyPath := getExpandedArg(v, StakingSignerKeyPathKey)
|
|
_, err := os.Stat(signingKeyPath)
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
signingKeyBytes, err := os.ReadFile(signingKeyPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
key, err := localsigner.FromBytes(signingKeyBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't parse signing key: %w", err)
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
if v.IsSet(StakingSignerKeyPathKey) {
|
|
return nil, errMissingStakingSigningKeyFile
|
|
}
|
|
|
|
key, err := localsigner.New()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't generate new signing key: %w", err)
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(signingKeyPath), perms.ReadWriteExecute); err != nil {
|
|
return nil, fmt.Errorf("couldn't create path for signing key at %s: %w", signingKeyPath, err)
|
|
}
|
|
|
|
keyBytes := key.ToBytes()
|
|
if err := os.WriteFile(signingKeyPath, keyBytes, perms.ReadWrite); err != nil {
|
|
return nil, fmt.Errorf("couldn't write new signing key to %s: %w", signingKeyPath, err)
|
|
}
|
|
if err := os.Chmod(signingKeyPath, perms.ReadOnly); err != nil {
|
|
return nil, fmt.Errorf("couldn't restrict permissions on new signing key at %s: %w", signingKeyPath, err)
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
// loadPEMBlock returns the body of a single PEM block matching
|
|
// expectType. Refuses to silently consume the wrong block type so a
|
|
// misnamed file (e.g. ML-DSA private key supplied as a public-key
|
|
// path) fails loud at config-load instead of producing a NodeID under
|
|
// the wrong key.
|
|
func loadPEMBlock(path, expectType string) ([]byte, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read %s: %w", path, err)
|
|
}
|
|
block, _ := pem.Decode(data)
|
|
if block == nil {
|
|
return nil, fmt.Errorf("%s: not a PEM file", path)
|
|
}
|
|
if block.Type != expectType {
|
|
return nil, fmt.Errorf("%s: PEM type %q is not %s", path, block.Type, expectType)
|
|
}
|
|
return block.Bytes, nil
|
|
}
|
|
|
|
// pemBytesOrFile resolves either a base64-encoded PEM in
|
|
// contentKey (highest precedence, matches the
|
|
// --foo-file-content / --foo-file pattern the existing TLS
|
|
// loaders use) or a filesystem path in pathKey. A set-but-empty
|
|
// contentKey is treated as absent and falls through to pathKey, so a
|
|
// blank content flag and a missing one behave identically. Empty
|
|
// return = neither was provided; caller decides whether that's a fatal
|
|
// config error or a "fall through to classical-compat" path.
|
|
func pemBytesOrFile(v *viper.Viper, contentKey, pathKey, expectType string) ([]byte, string, error) {
|
|
// A non-empty *-content flag wins. A set-but-empty one (e.g. an env var or
|
|
// a deploy template that rendered to "") is treated identically to an
|
|
// absent flag: fall through to the *-file path below. Short-circuiting on
|
|
// the empty value here would silently degrade a strict-PQ validator to a
|
|
// classical ECDSA NodeID — the strict-PQ activation outage this guards.
|
|
if raw := v.GetString(contentKey); raw != "" {
|
|
decoded, err := base64.StdEncoding.DecodeString(raw)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("decode %s base64: %w", contentKey, err)
|
|
}
|
|
block, _ := pem.Decode(decoded)
|
|
if block == nil {
|
|
return nil, "", fmt.Errorf("%s: decoded value is not PEM", contentKey)
|
|
}
|
|
if block.Type != expectType {
|
|
return nil, "", fmt.Errorf("%s: PEM type %q is not %s", contentKey, block.Type, expectType)
|
|
}
|
|
return block.Bytes, "<from-content>", nil
|
|
}
|
|
path := getExpandedArg(v, pathKey)
|
|
if path == "" {
|
|
return nil, "", nil
|
|
}
|
|
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
|
|
return nil, path, nil
|
|
} else if err != nil {
|
|
return nil, path, err
|
|
}
|
|
body, err := loadPEMBlock(path, expectType)
|
|
return body, path, err
|
|
}
|
|
|
|
// loadStakingMLDSA returns the strict-PQ ML-DSA-65 keypair (if any).
|
|
// Both private and public materials are optional at the config layer
|
|
// — a missing pair means classical-compat mode. A strict-PQ chain
|
|
// profile enforces presence at the validator-set boundary.
|
|
func loadStakingMLDSA(v *viper.Viper) (*mldsa.PrivateKey, []byte, string, string, error) {
|
|
privBytes, privPath, err := pemBytesOrFile(v,
|
|
StakingMLDSAKeyContentKey, StakingMLDSAKeyPathKey, "ML-DSA-65 PRIVATE KEY")
|
|
if err != nil {
|
|
return nil, nil, privPath, "", fmt.Errorf("staking-mldsa-key: %w", err)
|
|
}
|
|
pubBytes, pubPath, err := pemBytesOrFile(v,
|
|
StakingMLDSAPubKeyContentKey, StakingMLDSAPubKeyPathKey, "ML-DSA-65 PUBLIC KEY")
|
|
if err != nil {
|
|
return nil, nil, privPath, pubPath, fmt.Errorf("staking-mldsa-pub-key: %w", err)
|
|
}
|
|
if len(privBytes) == 0 && len(pubBytes) == 0 {
|
|
return nil, nil, privPath, pubPath, nil
|
|
}
|
|
// Asymmetry is a config error: a strict-PQ chain MUST have both
|
|
// the private key (for signing) and the public key (so peers can
|
|
// verify what we signed). Refuse rather than silently degrade.
|
|
if len(privBytes) == 0 || len(pubBytes) == 0 {
|
|
return nil, nil, privPath, pubPath, errors.New("staking-mldsa: both private and public key are required (or neither for classical-compat)")
|
|
}
|
|
priv, err := mldsa.PrivateKeyFromBytes(mldsa.MLDSA65, privBytes)
|
|
if err != nil {
|
|
return nil, nil, privPath, pubPath, fmt.Errorf("parse staking-mldsa private key: %w", err)
|
|
}
|
|
// Sanity: the public key on disk must match the one derivable
|
|
// from the private key. Mismatch would point a NodeID at a
|
|
// public key the validator cannot actually sign for — silent
|
|
// authentication failure that the consensus engine would later report as
|
|
// "this validator is offline" rather than "you misconfigured
|
|
// your keys".
|
|
derivedPubBytes := priv.PublicKey.Bytes()
|
|
if !bytes.Equal(derivedPubBytes, pubBytes) {
|
|
return nil, nil, privPath, pubPath, errors.New("staking-mldsa: public key on disk does not match the public key derived from the private key")
|
|
}
|
|
return priv, pubBytes, privPath, pubPath, nil
|
|
}
|
|
|
|
// loadHandshakeMLKEM returns the strict-PQ ML-KEM-768 KEM keypair
|
|
// (if any). Same shape + invariants as loadStakingMLDSA.
|
|
func loadHandshakeMLKEM(v *viper.Viper) (*mlkemcrypto.PrivateKey, []byte, string, string, error) {
|
|
privBytes, privPath, err := pemBytesOrFile(v,
|
|
HandshakeMLKEMKeyContentKey, HandshakeMLKEMKeyPathKey, "ML-KEM-768 PRIVATE KEY")
|
|
if err != nil {
|
|
return nil, nil, privPath, "", fmt.Errorf("handshake-mlkem-key: %w", err)
|
|
}
|
|
pubBytes, pubPath, err := pemBytesOrFile(v,
|
|
HandshakeMLKEMPubKeyContentKey, HandshakeMLKEMPubKeyPathKey, "ML-KEM-768 PUBLIC KEY")
|
|
if err != nil {
|
|
return nil, nil, privPath, pubPath, fmt.Errorf("handshake-mlkem-pub-key: %w", err)
|
|
}
|
|
if len(privBytes) == 0 && len(pubBytes) == 0 {
|
|
return nil, nil, privPath, pubPath, nil
|
|
}
|
|
if len(privBytes) == 0 || len(pubBytes) == 0 {
|
|
return nil, nil, privPath, pubPath, errors.New("handshake-mlkem: both private and public key are required (or neither for classical-compat)")
|
|
}
|
|
priv, err := mlkemcrypto.PrivateKeyFromBytes(privBytes, mlkemcrypto.MLKEM768)
|
|
if err != nil {
|
|
return nil, nil, privPath, pubPath, fmt.Errorf("parse handshake-mlkem private key: %w", err)
|
|
}
|
|
derivedPubBytes := priv.PublicKey().Bytes()
|
|
if !bytes.Equal(derivedPubBytes, pubBytes) {
|
|
return nil, nil, privPath, pubPath, errors.New("handshake-mlkem: public key on disk does not match the public key derived from the private key")
|
|
}
|
|
return priv, pubBytes, privPath, pubPath, nil
|
|
}
|
|
|
|
func getStakingConfig(v *viper.Viper, networkID uint32) (node.StakingConfig, error) {
|
|
config := node.StakingConfig{
|
|
SybilProtectionEnabled: v.GetBool(SybilProtectionEnabledKey),
|
|
SybilProtectionDisabledWeight: v.GetUint64(SybilProtectionDisabledWeightKey),
|
|
PartialSyncPrimaryNetwork: v.GetBool(PartialSyncPrimaryNetworkKey),
|
|
StakingKeyPath: getExpandedArg(v, StakingTLSKeyPathKey),
|
|
StakingCertPath: getExpandedArg(v, StakingCertPathKey),
|
|
StakingSignerPath: getExpandedArg(v, StakingSignerKeyPathKey),
|
|
}
|
|
if !config.SybilProtectionEnabled && config.SybilProtectionDisabledWeight == 0 {
|
|
return node.StakingConfig{}, errSybilProtectionDisabledStakerWeights
|
|
}
|
|
|
|
if !config.SybilProtectionEnabled && (networkID == constants.MainnetID || networkID == constants.TestnetID) && !v.GetBool(DevModeKey) {
|
|
return node.StakingConfig{}, errSybilProtectionDisabledOnPublicNetwork
|
|
}
|
|
|
|
var err error
|
|
config.StakingTLSCert, err = getStakingTLSCert(v)
|
|
if err != nil {
|
|
return node.StakingConfig{}, err
|
|
}
|
|
config.StakingSigningKey, err = getStakingSigner(v)
|
|
if err != nil {
|
|
return node.StakingConfig{}, err
|
|
}
|
|
|
|
// Strict-PQ identity (FIPS 204 ML-DSA-65 + FIPS 203 ML-KEM-768).
|
|
// Both pairs are optional at this layer — classical-compat chains
|
|
// run without them. Strict-PQ profile rejects a missing pair at
|
|
// the validator-set boundary; that gate lives in consensus/config
|
|
// (the profile gate that owns the strict-PQ vs classical-compat
|
|
// dispatch), not here in node-config land.
|
|
mldsaPriv, mldsaPub, mldsaPrivPath, mldsaPubPath, err := loadStakingMLDSA(v)
|
|
if err != nil {
|
|
return node.StakingConfig{}, err
|
|
}
|
|
config.StakingMLDSA = mldsaPriv
|
|
config.StakingMLDSAPub = mldsaPub
|
|
config.StakingMLDSAKeyPath = mldsaPrivPath
|
|
config.StakingMLDSAPubPath = mldsaPubPath
|
|
|
|
mlkemPriv, mlkemPub, mlkemPrivPath, mlkemPubPath, err := loadHandshakeMLKEM(v)
|
|
if err != nil {
|
|
return node.StakingConfig{}, err
|
|
}
|
|
config.HandshakeMLKEMPriv = mlkemPriv
|
|
config.HandshakeMLKEMPub = mlkemPub
|
|
config.HandshakeMLKEMKeyPath = mlkemPrivPath
|
|
config.HandshakeMLKEMPubPath = mlkemPubPath
|
|
|
|
if networkID != constants.MainnetID && networkID != constants.TestnetID {
|
|
config.UptimeRequirement = v.GetFloat64(UptimeRequirementKey)
|
|
config.MinValidatorStake = v.GetUint64(MinValidatorStakeKey)
|
|
config.MaxValidatorStake = v.GetUint64(MaxValidatorStakeKey)
|
|
config.MinDelegatorStake = v.GetUint64(MinDelegatorStakeKey)
|
|
config.MinStakeDuration = v.GetDuration(MinStakeDurationKey)
|
|
config.MaxStakeDuration = v.GetDuration(MaxStakeDurationKey)
|
|
config.RewardConfig.MaxConsumptionRate = v.GetUint64(StakeMaxConsumptionRateKey)
|
|
config.RewardConfig.MinConsumptionRate = v.GetUint64(StakeMinConsumptionRateKey)
|
|
config.RewardConfig.MintingPeriod = v.GetDuration(StakeMintingPeriodKey)
|
|
config.RewardConfig.SupplyCap = v.GetUint64(StakeSupplyCapKey)
|
|
config.MinDelegationFee = v.GetUint32(MinDelegatorFeeKey)
|
|
switch {
|
|
case config.UptimeRequirement < 0 || config.UptimeRequirement > 1:
|
|
return node.StakingConfig{}, errInvalidUptimeRequirement
|
|
case config.MinValidatorStake > config.MaxValidatorStake:
|
|
return node.StakingConfig{}, errMinValidatorStakeAboveMax
|
|
case config.MinDelegationFee > 1_000_000:
|
|
return node.StakingConfig{}, errInvalidDelegationFee
|
|
case config.MinStakeDuration <= 0:
|
|
return node.StakingConfig{}, errInvalidMinStakeDuration
|
|
case config.MaxStakeDuration < config.MinStakeDuration:
|
|
return node.StakingConfig{}, errMinStakeDurationAboveMax
|
|
case config.RewardConfig.MaxConsumptionRate > reward.PercentDenominator:
|
|
return node.StakingConfig{}, errStakeMaxConsumptionTooLarge
|
|
case config.RewardConfig.MaxConsumptionRate < config.RewardConfig.MinConsumptionRate:
|
|
return node.StakingConfig{}, errStakeMaxConsumptionBelowMin
|
|
case config.RewardConfig.MintingPeriod < config.MaxStakeDuration:
|
|
return node.StakingConfig{}, errStakeMintingPeriodBelowMin
|
|
}
|
|
} else {
|
|
config.StakingConfig = builder.GetStakingConfig(networkID)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func getTxFeeConfig(v *viper.Viper, networkID uint32) builder.TxFeeConfig {
|
|
if networkID != constants.MainnetID && networkID != constants.TestnetID {
|
|
return builder.TxFeeConfig{
|
|
CreateAssetTxFee: v.GetUint64(CreateAssetTxFeeKey),
|
|
TxFee: v.GetUint64(TxFeeKey),
|
|
DynamicFeeConfig: gas.Config{
|
|
Weights: gas.Dimensions{
|
|
gas.Bandwidth: v.GetUint64(DynamicFeesBandwidthWeightKey),
|
|
gas.DBRead: v.GetUint64(DynamicFeesDBReadWeightKey),
|
|
gas.DBWrite: v.GetUint64(DynamicFeesDBWriteWeightKey),
|
|
gas.Compute: v.GetUint64(DynamicFeesComputeWeightKey),
|
|
},
|
|
MaxCapacity: gas.Gas(v.GetUint64(DynamicFeesMaxGasCapacityKey)),
|
|
MaxPerSecond: gas.Gas(v.GetUint64(DynamicFeesMaxGasPerSecondKey)),
|
|
TargetPerSecond: gas.Gas(v.GetUint64(DynamicFeesTargetGasPerSecondKey)),
|
|
MinPrice: gas.Price(v.GetUint64(DynamicFeesMinGasPriceKey)),
|
|
ExcessConversionConstant: gas.Gas(v.GetUint64(DynamicFeesExcessConversionConstantKey)),
|
|
},
|
|
ValidatorFeeConfig: fee.Config{
|
|
Capacity: gas.Gas(v.GetUint64(ValidatorFeesCapacityKey)),
|
|
Target: gas.Gas(v.GetUint64(ValidatorFeesTargetKey)),
|
|
MinPrice: gas.Price(v.GetUint64(ValidatorFeesMinPriceKey)),
|
|
ExcessConversionConstant: gas.Gas(v.GetUint64(ValidatorFeesExcessConversionConstantKey)),
|
|
},
|
|
}
|
|
}
|
|
return builder.GetTxFeeConfig(networkID)
|
|
}
|
|
|
|
func getUpgradeConfig(v *viper.Viper, networkID uint32) (upgrade.Config, error) {
|
|
if !v.IsSet(UpgradeFileKey) && !v.IsSet(UpgradeFileContentKey) {
|
|
return upgrade.GetConfig(networkID), nil
|
|
}
|
|
|
|
// Reject upgrade overrides on the well-known networks where the
|
|
// upgrade schedule is hard-coded by luxfi/upgrade. Any other ID
|
|
// (DevnetID, UnitTestID, or genuinely custom user-defined network
|
|
// IDs) is permitted to supply its own upgrade schedule.
|
|
switch networkID {
|
|
case constants.MainnetID, constants.TestnetID, constants.LocalID:
|
|
return upgrade.Config{}, fmt.Errorf("cannot configure upgrades for networkID: %s",
|
|
constants.NetworkName(networkID),
|
|
)
|
|
}
|
|
|
|
var (
|
|
upgradeBytes []byte
|
|
err error
|
|
)
|
|
switch {
|
|
case v.IsSet(UpgradeFileKey):
|
|
upgradeFileName := getExpandedArg(v, UpgradeFileKey)
|
|
upgradeBytes, err = os.ReadFile(upgradeFileName)
|
|
if err != nil {
|
|
return upgrade.Config{}, fmt.Errorf("unable to read upgrade file: %w", err)
|
|
}
|
|
case v.IsSet(UpgradeFileContentKey):
|
|
upgradeContent := v.GetString(UpgradeFileContentKey)
|
|
upgradeBytes, err = base64.StdEncoding.DecodeString(upgradeContent)
|
|
if err != nil {
|
|
return upgrade.Config{}, fmt.Errorf("unable to decode upgrade base64 content: %w", err)
|
|
}
|
|
}
|
|
|
|
var upgradeConfig upgrade.Config
|
|
if err := json.Unmarshal(upgradeBytes, &upgradeConfig, json.MatchCaseInsensitiveNames(true)); err != nil {
|
|
return upgrade.Config{}, fmt.Errorf("unable to unmarshal upgrade bytes: %w", err)
|
|
}
|
|
return upgradeConfig, nil
|
|
}
|
|
|
|
func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.StakingConfig, dataDir string) ([]byte, ids.ID, error) {
|
|
// (Removed: automine-mode genesis swap.) --automine is single-node
|
|
// consensus only; it falls through to the canonical genesis loader
|
|
// below — same as a real network. C-Chain genesis comes from the
|
|
// canonical luxfi/genesis embedded config or --genesis-file. Period.
|
|
|
|
// HIGHEST PRIORITY: Raw genesis bytes - use directly without rebuilding
|
|
// This is critical for snapshot resume to avoid hash mismatch
|
|
if v.IsSet(GenesisRawBytesKey) {
|
|
genesisB64 := v.GetString(GenesisRawBytesKey)
|
|
genesisBytes, err := base64.StdEncoding.DecodeString(genesisB64)
|
|
if err != nil {
|
|
return nil, ids.Empty, fmt.Errorf("failed to decode %s: %w", GenesisRawBytesKey, err)
|
|
}
|
|
utxoAssetID, err := resolveUTXOAssetID(networkID, genesisBytes)
|
|
if err != nil {
|
|
return nil, ids.Empty, fmt.Errorf("resolve X-Chain asset ID from %s: %w", GenesisRawBytesKey, err)
|
|
}
|
|
log.Info("loaded raw genesis bytes directly",
|
|
"size", len(genesisBytes),
|
|
"utxoAssetID", utxoAssetID,
|
|
)
|
|
return genesisBytes, utxoAssetID, nil
|
|
}
|
|
|
|
// Check if genesis-db is specified for database replay
|
|
if v.IsSet(GenesisDBKey) {
|
|
if v.IsSet(GenesisFileKey) || v.IsSet(GenesisFileContentKey) {
|
|
return nil, ids.Empty, fmt.Errorf("cannot specify %s with %s or %s", GenesisDBKey, GenesisFileKey, GenesisFileContentKey)
|
|
}
|
|
genesisDBPath := getExpandedArg(v, GenesisDBKey)
|
|
genesisDBType := v.GetString(GenesisDBTypeKey)
|
|
|
|
// Auto-detect database type based on path if not specified
|
|
if genesisDBType == "" {
|
|
genesisDBType = "zapdb" // Default is always zapdb
|
|
}
|
|
|
|
return builder.FromDatabase(networkID, genesisDBPath, genesisDBType, stakingCfg)
|
|
}
|
|
|
|
// try first loading genesis content directly from flag/env-var
|
|
if v.IsSet(GenesisFileContentKey) {
|
|
genesisData := v.GetString(GenesisFileContentKey)
|
|
return builder.FromFlag(networkID, genesisData, stakingCfg)
|
|
}
|
|
|
|
// if content is not specified go for the file
|
|
if v.IsSet(GenesisFileKey) {
|
|
genesisFileName := getExpandedArg(v, GenesisFileKey)
|
|
// Check if we have cached genesis bytes to avoid rebuilding
|
|
cacheFile := filepath.Join(dataDir, "genesis.bytes")
|
|
if cachedBytes, err := loadCachedGenesisBytes(cacheFile); err == nil && len(cachedBytes) > 0 {
|
|
utxoAssetID, err := resolveUTXOAssetID(networkID, cachedBytes)
|
|
if err == nil {
|
|
log.Info("loaded cached genesis bytes for hash stability",
|
|
"cacheFile", cacheFile,
|
|
"size", len(cachedBytes),
|
|
"utxoAssetID", utxoAssetID,
|
|
)
|
|
return cachedBytes, utxoAssetID, nil
|
|
}
|
|
// Cache parse failed — almost certainly a codec mismatch
|
|
// after a binary upgrade (e.g. v1-codec bytes persisted by
|
|
// an older luxd, multi-version v0+v1 dispatcher in this
|
|
// luxd doesn't recognise a type the cached blob still
|
|
// uses). Drop the cache and rebuild from the file rather
|
|
// than wedging in a CrashLoop. Hash stability is forfeit
|
|
// for this single restart — intentional, the alternative
|
|
// is a permanent outage on every binary bump.
|
|
log.Warn("cached genesis bytes failed to parse — invalidating cache and rebuilding from genesis-file",
|
|
"cacheFile", cacheFile,
|
|
"size", len(cachedBytes),
|
|
"error", err,
|
|
)
|
|
_ = os.Remove(cacheFile)
|
|
}
|
|
// No cache or invalid cache - build from file and cache the result
|
|
genesisBytes, utxoAssetID, err := builder.FromFile(networkID, genesisFileName, stakingCfg)
|
|
if err != nil {
|
|
return nil, ids.Empty, err
|
|
}
|
|
// Cache the built bytes for future restarts
|
|
if err := saveCachedGenesisBytes(cacheFile, genesisBytes); err != nil {
|
|
log.Warn("failed to cache genesis bytes (hash stability may be affected on restart)",
|
|
"error", err,
|
|
)
|
|
} else {
|
|
log.Info("cached genesis bytes for hash stability",
|
|
"cacheFile", cacheFile,
|
|
"size", len(genesisBytes),
|
|
)
|
|
}
|
|
return genesisBytes, utxoAssetID, nil
|
|
}
|
|
|
|
// finally if file is not specified/readable go for the predefined config
|
|
config := builder.GetConfig(networkID)
|
|
return builder.FromConfig(config)
|
|
}
|
|
|
|
// resolveUTXOAssetID extracts the X-Chain native asset ID from the loaded
|
|
// platform-genesis blob. It is the canonical source of truth used by
|
|
// every load path that bypasses FromConfig (raw bytes, cached bytes —
|
|
// the paths that historically defaulted to constants.UTXOAssetIDFor and
|
|
// silently disagreed with the genesis content on sovereign L1s).
|
|
//
|
|
// Behaviour:
|
|
//
|
|
// - If the genesis bakes an X-Chain, returns the runtime asset ID
|
|
// derived from that chain's CreateAssetTx (the same value
|
|
// vm.initGenesis assigns at runtime).
|
|
// - If the genesis is P-only (no X-Chain), returns the network-id-
|
|
// keyed constant. The asset ID is unused in that mode.
|
|
// - If the genesis is unparseable or the embedded X-Chain genesis
|
|
// is malformed, returns the corresponding error — these are
|
|
// unrecoverable on a primary-network bootstrap.
|
|
//
|
|
// Tested against both sovereign-network genesis (X-Chain present, asset
|
|
// ID differs from the constant) and upstream Lux genesis fixtures.
|
|
func resolveUTXOAssetID(networkID uint32, genesisBytes []byte) (ids.ID, error) {
|
|
id, ok, err := builder.UTXOAssetIDFromGenesisBytes(genesisBytes)
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
if !ok {
|
|
return constants.UTXOAssetIDFor(networkID), nil
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
func getTrackedChains(v *viper.Viper) (set.Set[ids.ID], error) {
|
|
trackChainsStr := v.GetString(TrackChainsKey)
|
|
|
|
// Special value "all" means track all chains dynamically
|
|
// This is indicated by returning nil (handled specially by chain manager)
|
|
if strings.ToLower(strings.TrimSpace(trackChainsStr)) == "all" {
|
|
return nil, nil // nil set means "track all"
|
|
}
|
|
|
|
trackChainsStrs := strings.Split(trackChainsStr, ",")
|
|
trackedChainIDs := set.NewSet[ids.ID](len(trackChainsStrs))
|
|
|
|
for _, chain := range trackChainsStrs {
|
|
if chain == "" {
|
|
continue
|
|
}
|
|
|
|
// Parse chain ID
|
|
chainID, err := ids.FromString(chain)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't parse chainID %q: %w", chain, err)
|
|
}
|
|
if chainID == constants.PrimaryNetworkID {
|
|
return nil, errCannotTrackPrimaryNetwork
|
|
}
|
|
trackedChainIDs.Add(chainID)
|
|
}
|
|
return trackedChainIDs, nil
|
|
}
|
|
|
|
func getDatabaseConfig(v *viper.Viper, networkID uint32) (node.DatabaseConfig, error) {
|
|
var (
|
|
configBytes []byte
|
|
err error
|
|
)
|
|
if v.IsSet(DBConfigContentKey) {
|
|
dbConfigContent := v.GetString(DBConfigContentKey)
|
|
configBytes, err = base64.StdEncoding.DecodeString(dbConfigContent)
|
|
if err != nil {
|
|
return node.DatabaseConfig{}, fmt.Errorf("unable to decode base64 content: %w", err)
|
|
}
|
|
} else if v.IsSet(DBConfigFileKey) {
|
|
path := getExpandedArg(v, DBConfigFileKey)
|
|
configBytes, err = os.ReadFile(path)
|
|
if err != nil {
|
|
return node.DatabaseConfig{}, err
|
|
}
|
|
} else {
|
|
// Build ZapDB config from low memory settings if enabled
|
|
configBytes, err = buildDatabaseConfigBytes(v)
|
|
if err != nil {
|
|
return node.DatabaseConfig{}, fmt.Errorf("failed to build database config: %w", err)
|
|
}
|
|
}
|
|
|
|
return node.DatabaseConfig{
|
|
Name: v.GetString(DBTypeKey),
|
|
ReadOnly: v.GetBool(DBReadOnlyKey),
|
|
Path: filepath.Join(
|
|
getExpandedArg(v, DBPathKey),
|
|
constants.NetworkName(networkID),
|
|
),
|
|
Config: configBytes,
|
|
}, nil
|
|
}
|
|
|
|
// buildDatabaseConfigBytes creates ZapDB config JSON from viper settings.
|
|
// When low-memory or dev-light mode is enabled, it generates a config with
|
|
// reduced memory footprint suitable for local development.
|
|
func buildDatabaseConfigBytes(v *viper.Viper) ([]byte, error) {
|
|
lowMemConfig := GetLowMemoryConfig(v)
|
|
|
|
// Only generate config if low memory mode is enabled or specific DB settings are set
|
|
if !lowMemConfig.Enabled && !v.IsSet(DBCacheSizeKey) && !v.IsSet(DBMemtableSizeKey) {
|
|
return nil, nil
|
|
}
|
|
|
|
// ZapDB config structure matching database/zapdb/db.go Config struct
|
|
// Field names must match exactly (camelCase JSON tags)
|
|
type badgerConfig struct {
|
|
SyncWrites bool `json:"syncWrites"`
|
|
NumCompactors int `json:"numCompactors"`
|
|
NumMemtables int `json:"numMemtables"`
|
|
MemTableSize int64 `json:"memTableSize"`
|
|
BlockCacheSize int64 `json:"blockCacheSize"`
|
|
IndexCacheSize int64 `json:"indexCacheSize"`
|
|
BloomFalsePositive float64 `json:"bloomFalsePositive"`
|
|
}
|
|
|
|
cfg := badgerConfig{
|
|
SyncWrites: false,
|
|
NumCompactors: 2, // Reduced from 4
|
|
NumMemtables: 2, // Reduced from 5
|
|
MemTableSize: int64(lowMemConfig.DBMemtableSize), // 8 MB vs 64 MB default
|
|
BlockCacheSize: int64(lowMemConfig.DBCacheSize), // 8 MB vs 256 MB default
|
|
IndexCacheSize: int64(lowMemConfig.DBCacheSize / 2), // 4 MB vs 100 MB default
|
|
BloomFalsePositive: 0.1, // 10% vs 1% (saves memory)
|
|
}
|
|
|
|
// If bloom filters are disabled, set very high false positive rate
|
|
if lowMemConfig.DisableBloomFilters {
|
|
cfg.BloomFalsePositive = 1.0 // Effectively disables bloom filters
|
|
}
|
|
|
|
return json.Marshal(cfg)
|
|
}
|
|
|
|
func getAliases(v *viper.Viper, name string, contentKey string, fileKey string) (map[ids.ID][]string, error) {
|
|
var fileBytes []byte
|
|
if v.IsSet(contentKey) {
|
|
var err error
|
|
aliasFlagContent := v.GetString(contentKey)
|
|
fileBytes, err = base64.StdEncoding.DecodeString(aliasFlagContent)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode base64 content for %s: %w", name, err)
|
|
}
|
|
} else {
|
|
aliasFilePath := filepath.Clean(getExpandedArg(v, fileKey))
|
|
exists, err := storage.FileExists(aliasFilePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !exists {
|
|
if v.IsSet(fileKey) {
|
|
return nil, fmt.Errorf("%w: %s", errFileDoesNotExist, aliasFilePath)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
fileBytes, err = os.ReadFile(aliasFilePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
aliasMap := make(map[ids.ID][]string)
|
|
if err := json.Unmarshal(fileBytes, &aliasMap, json.MatchCaseInsensitiveNames(true)); err != nil {
|
|
return nil, fmt.Errorf("%w on %s: %w", errUnmarshalling, name, err)
|
|
}
|
|
return aliasMap, nil
|
|
}
|
|
|
|
func getVMAliases(v *viper.Viper) (map[ids.ID][]string, error) {
|
|
return getAliases(v, "vm aliases", VMAliasesContentKey, VMAliasesFileKey)
|
|
}
|
|
|
|
func getChainAliases(v *viper.Viper) (map[ids.ID][]string, error) {
|
|
return getAliases(v, "chain aliases", ChainAliasesContentKey, ChainAliasesFileKey)
|
|
}
|
|
|
|
// getPathFromDirKey reads flag value from viper instance and then checks the folder existence
|
|
func getPathFromDirKey(v *viper.Viper, configKey string) (string, error) {
|
|
configDir := getExpandedArg(v, configKey)
|
|
cleanPath := filepath.Clean(configDir)
|
|
ok, err := storage.FolderExists(cleanPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if ok {
|
|
return cleanPath, nil
|
|
}
|
|
if v.IsSet(configKey) {
|
|
// user specified a config dir explicitly, but dir does not exist.
|
|
return "", fmt.Errorf("%w: %s", errCannotReadDirectory, cleanPath)
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func getChainConfigsFromFlag(v *viper.Viper) (map[string]chains.ChainConfig, error) {
|
|
chainConfigContentB64 := v.GetString(ChainConfigContentKey)
|
|
chainConfigContent, err := base64.StdEncoding.DecodeString(chainConfigContentB64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode base64 content: %w", err)
|
|
}
|
|
|
|
chainConfigs := make(map[string]chains.ChainConfig)
|
|
if err := json.Unmarshal(chainConfigContent, &chainConfigs, json.MatchCaseInsensitiveNames(true)); err != nil {
|
|
return nil, fmt.Errorf("could not unmarshal JSON: %w", err)
|
|
}
|
|
return chainConfigs, nil
|
|
}
|
|
|
|
func getChainConfigsFromDir(v *viper.Viper) (map[string]chains.ChainConfig, error) {
|
|
chainConfigPath, err := getPathFromDirKey(v, ChainConfigDirKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(chainConfigPath) == 0 {
|
|
return make(map[string]chains.ChainConfig), nil
|
|
}
|
|
|
|
return readChainConfigPath(chainConfigPath)
|
|
}
|
|
|
|
// getChainConfigs reads & puts chainConfigs to node config
|
|
func getChainConfigs(v *viper.Viper) (map[string]chains.ChainConfig, error) {
|
|
if v.IsSet(ChainConfigContentKey) {
|
|
return getChainConfigsFromFlag(v)
|
|
}
|
|
return getChainConfigsFromDir(v)
|
|
}
|
|
|
|
// readChainConfigPath reads chain config files from static directories and returns map with contents,
|
|
// if successful.
|
|
func readChainConfigPath(chainConfigPath string) (map[string]chains.ChainConfig, error) {
|
|
chainDirs, err := filepath.Glob(filepath.Join(chainConfigPath, "*"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chainConfigMap := make(map[string]chains.ChainConfig)
|
|
for _, chainDir := range chainDirs {
|
|
dirInfo, err := os.Stat(chainDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !dirInfo.IsDir() {
|
|
continue
|
|
}
|
|
|
|
// chainconfigdir/chainId/config.*
|
|
configData, err := storage.ReadFileWithName(chainDir, chainConfigFileName)
|
|
if err != nil {
|
|
return chainConfigMap, err
|
|
}
|
|
|
|
// chainconfigdir/chainId/upgrade.*
|
|
upgradeData, err := storage.ReadFileWithName(chainDir, chainUpgradeFileName)
|
|
if err != nil {
|
|
return chainConfigMap, err
|
|
}
|
|
|
|
chainConfigMap[dirInfo.Name()] = chains.ChainConfig{
|
|
Config: configData,
|
|
Upgrade: upgradeData,
|
|
}
|
|
}
|
|
return chainConfigMap, nil
|
|
}
|
|
|
|
// getNetConfigs reads net configs from the correct place
|
|
// (flag or file) and returns a non-nil map.
|
|
func getNetConfigs(v *viper.Viper, netIDs []ids.ID) (map[ids.ID]nets.Config, error) {
|
|
if v.IsSet(NetConfigContentKey) {
|
|
return getNetConfigsFromFlags(v, netIDs)
|
|
}
|
|
return getNetConfigsFromDir(v, netIDs)
|
|
}
|
|
|
|
func getNetConfigsFromFlags(v *viper.Viper, netIDs []ids.ID) (map[ids.ID]nets.Config, error) {
|
|
netConfigContentB64 := v.GetString(NetConfigContentKey)
|
|
netConfigContent, err := base64.StdEncoding.DecodeString(netConfigContentB64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode base64 content: %w", err)
|
|
}
|
|
|
|
// partially parse configs to be filled by defaults later
|
|
chainConfigs := make(map[ids.ID]jsontext.Value, len(netIDs))
|
|
if err := json.Unmarshal(netConfigContent, &chainConfigs, json.MatchCaseInsensitiveNames(true)); err != nil {
|
|
return nil, fmt.Errorf("could not unmarshal JSON: %w", err)
|
|
}
|
|
|
|
res := make(map[ids.ID]nets.Config)
|
|
for _, chainID := range netIDs {
|
|
config := getDefaultNetConfig(v)
|
|
|
|
if rawNetConfigBytes, ok := chainConfigs[chainID]; ok {
|
|
if err := json.Unmarshal(rawNetConfigBytes, &config, json.MatchCaseInsensitiveNames(true)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Alpha override disabled - field not available in consensus.Parameters
|
|
// if config.ConsensusParameters.Alpha != nil && *config.ConsensusParameters.Alpha > 0 {
|
|
// config.ConsensusParameters.AlphaPreference = *config.ConsensusParameters.Alpha
|
|
// config.ConsensusParameters.AlphaConfidence = config.ConsensusParameters.AlphaPreference
|
|
// }
|
|
|
|
if err := config.Valid(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
res[chainID] = config
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// getNetConfigsFromDir reads NetConfigs to node config map
|
|
func getNetConfigsFromDir(v *viper.Viper, chainIDs []ids.ID) (map[ids.ID]nets.Config, error) {
|
|
chainConfigPath, err := getPathFromDirKey(v, NetConfigDirKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chainConfigs := make(map[ids.ID]nets.Config)
|
|
|
|
// reads chain config files from a path and given chainIDs and returns a map.
|
|
for _, chainID := range chainIDs {
|
|
// Ensure default configuration
|
|
config := getDefaultNetConfig(v)
|
|
chainConfigs[chainID] = config
|
|
|
|
if len(chainConfigPath) == 0 {
|
|
// chain config path does not exist but not explicitly specified, so ignore it
|
|
continue
|
|
}
|
|
|
|
filePath := filepath.Join(chainConfigPath, chainID.String()+chainConfigFileExt)
|
|
fileInfo, err := os.Stat(filePath)
|
|
switch {
|
|
case errors.Is(err, os.ErrNotExist):
|
|
// this chain config does not exist, the default configuration will be used
|
|
continue
|
|
case err != nil:
|
|
return nil, err
|
|
case fileInfo.IsDir():
|
|
return nil, fmt.Errorf("%q is a directory, expected a file", fileInfo.Name())
|
|
}
|
|
|
|
// netConfigDir/netID.json
|
|
file, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Update the default config with the values from the file
|
|
if err := json.Unmarshal(file, &config, json.MatchCaseInsensitiveNames(true)); err != nil {
|
|
return nil, fmt.Errorf("%w: %w", errUnmarshalling, err)
|
|
}
|
|
|
|
// Only override if Alpha is explicitly set (not zero)
|
|
// Alpha override disabled - field not available in consensus.Parameters
|
|
// if config.ConsensusParameters.Alpha != nil && *config.ConsensusParameters.Alpha > 0 {
|
|
// config.ConsensusParameters.AlphaPreference = *config.ConsensusParameters.Alpha
|
|
// config.ConsensusParameters.AlphaConfidence = config.ConsensusParameters.AlphaPreference
|
|
// }
|
|
|
|
if err := config.Valid(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chainConfigs[chainID] = config
|
|
}
|
|
|
|
return chainConfigs, nil
|
|
}
|
|
|
|
func getDefaultNetConfig(v *viper.Viper) nets.Config {
|
|
config := nets.Config{
|
|
ConsensusParameters: getConsensusConfig(v),
|
|
ValidatorOnly: false,
|
|
ProposerMinBlockDelay: v.GetDuration(ProposerVMMinBlockDelayKey),
|
|
ProposerNumHistoricalBlocks: proposervm.DefaultNumHistoricalBlocks,
|
|
POAEnabled: v.GetBool(DevModeKey) || v.GetBool(POAModeEnabledKey),
|
|
POASingleNodeMode: v.GetBool(DevModeKey) || v.GetBool(POASingleNodeModeKey),
|
|
POAMinBlockTime: v.GetDuration(POAMinBlockTimeKey),
|
|
}
|
|
|
|
// If automine mode or POA mode is enabled, adjust consensus parameters
|
|
if config.POAEnabled {
|
|
config.ConsensusParameters = nets.GetPOAConsensusParameters()
|
|
if config.POAMinBlockTime == 0 {
|
|
config.POAMinBlockTime = 1 * time.Second
|
|
}
|
|
config.ProposerMinBlockDelay = config.POAMinBlockTime
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
func getDiskSpaceConfig(v *viper.Viper) (requiredAvailableDiskSpace uint64, warningThresholdAvailableDiskSpace uint64, err error) {
|
|
requiredAvailableDiskSpace = v.GetUint64(SystemTrackerRequiredAvailableDiskSpaceKey)
|
|
warningThresholdAvailableDiskSpace = v.GetUint64(SystemTrackerWarningThresholdAvailableDiskSpaceKey)
|
|
switch {
|
|
case warningThresholdAvailableDiskSpace < requiredAvailableDiskSpace:
|
|
return 0, 0, fmt.Errorf("%q (%d) < %q (%d)", SystemTrackerWarningThresholdAvailableDiskSpaceKey, warningThresholdAvailableDiskSpace, SystemTrackerRequiredAvailableDiskSpaceKey, requiredAvailableDiskSpace)
|
|
default:
|
|
return requiredAvailableDiskSpace, warningThresholdAvailableDiskSpace, nil
|
|
}
|
|
}
|
|
|
|
func getTraceConfig(v *viper.Viper) (trace.Config, error) {
|
|
exporterTypeStr := v.GetString(TracingExporterTypeKey)
|
|
exporterType, err := trace.ExporterTypeFromString(exporterTypeStr)
|
|
if err != nil {
|
|
return trace.Config{}, err
|
|
}
|
|
|
|
return trace.Config{
|
|
ExporterConfig: trace.ExporterConfig{
|
|
Type: exporterType,
|
|
Endpoint: v.GetString(TracingEndpointKey),
|
|
Insecure: v.GetBool(TracingInsecureKey),
|
|
Headers: v.GetStringMapString(TracingHeadersKey),
|
|
},
|
|
TraceSampleRate: v.GetFloat64(TracingSampleRateKey),
|
|
AppName: constants.AppName,
|
|
Version: version.Current.String(),
|
|
}, nil
|
|
}
|
|
|
|
// Returns the path to the directory that contains VM binaries.
|
|
func getPluginDir(v *viper.Viper) (string, error) {
|
|
pluginDir := getExpandedArg(v, PluginDirKey)
|
|
|
|
if v.IsSet(PluginDirKey) {
|
|
// If the flag was given, assert it exists and is a directory
|
|
info, err := os.Stat(pluginDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("plugin dir %q not found: %w", pluginDir, err)
|
|
}
|
|
if !info.IsDir() {
|
|
return "", fmt.Errorf("%w: %q", errPluginDirNotADirectory, pluginDir)
|
|
}
|
|
} else {
|
|
// If the flag wasn't given, make sure the default location exists.
|
|
if err := os.MkdirAll(pluginDir, perms.ReadWriteExecute); err != nil {
|
|
return "", fmt.Errorf("failed to create plugin dir at %s: %w", pluginDir, err)
|
|
}
|
|
}
|
|
|
|
return pluginDir, nil
|
|
}
|
|
|
|
func GetNodeConfig(v *viper.Viper) (node.Config, error) {
|
|
var (
|
|
nodeConfig node.Config
|
|
err error
|
|
)
|
|
|
|
// Handle --config-profile flag first (lowest priority)
|
|
if profileName := v.GetString(ConfigProfileKey); profileName != "" {
|
|
profile, err := LoadConfigProfile(profileName)
|
|
if err != nil {
|
|
return node.Config{}, fmt.Errorf("failed to load config profile: %w", err)
|
|
}
|
|
ApplyConfigProfile(v, profile)
|
|
}
|
|
|
|
// Handle --dev-light flag (equivalent to --dev --low-memory)
|
|
if v.GetBool(DevLightKey) {
|
|
if err := ApplyDevLightMode(v); err != nil {
|
|
return node.Config{}, fmt.Errorf("failed to apply dev-light mode: %w", err)
|
|
}
|
|
}
|
|
|
|
// Handle --dev flag first
|
|
if v.GetBool(DevModeKey) {
|
|
// Development mode sets various flags for single-node operation
|
|
v.Set(SybilProtectionEnabledKey, false)
|
|
v.Set(SybilProtectionDisabledWeightKey, 100)
|
|
v.Set(ConsensusSampleSizeKey, 1)
|
|
v.Set(ConsensusQuorumSizeKey, 1)
|
|
// v.Set(ConsensusVirtuousCommitThresholdKey, 1) // Removed - key no longer exists
|
|
// v.Set(ConsensusRogueCommitThresholdKey, 1) // Removed - key no longer exists
|
|
v.Set(POASingleNodeModeKey, true)
|
|
v.Set(SkipBootstrapKey, true)
|
|
v.Set(NetworkHealthMinPeersKey, 0)
|
|
// Enable automining for anvil-like behavior (auto-produce blocks on transactions)
|
|
v.Set(EnableAutominingKey, true)
|
|
// Use custom network (ID 1337) by default for automine mode unless explicitly set
|
|
// This gives a standard dev chain ID like Hardhat/Anvil (1337)
|
|
if !v.IsSet(NetworkNameKey) {
|
|
v.Set(NetworkNameKey, "custom") // maps to network ID 1337
|
|
}
|
|
// Use ephemeral staking credentials for automine mode unless explicitly set
|
|
// This avoids needing to match genesis validators with local staking keys
|
|
if !v.IsSet(StakingEphemeralCertEnabledKey) && !v.IsSet(StakingTLSKeyContentKey) && !v.IsSet(StakingTLSKeyPathKey) {
|
|
v.Set(StakingEphemeralCertEnabledKey, true)
|
|
}
|
|
if !v.IsSet(StakingEphemeralSignerEnabledKey) && !v.IsSet(StakingSignerKeyContentKey) && !v.IsSet(StakingSignerKeyPathKey) {
|
|
v.Set(StakingEphemeralSignerEnabledKey, true)
|
|
}
|
|
// Use port 8545 for automine mode (standard Ethereum RPC port, like Anvil/Hardhat)
|
|
// This avoids conflicts with mainnet (9630), testnet (9640), devnet (9650) ports
|
|
if !v.IsSet(HTTPPortKey) {
|
|
v.Set(HTTPPortKey, 8545)
|
|
}
|
|
}
|
|
|
|
nodeConfig.PluginDir, err = getPluginDir(v)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
|
|
// GPU configuration - must be initialized early before any GPU accelerators
|
|
gpuConfig := GPUConfig{
|
|
Enabled: v.GetBool(GPUEnabledKey),
|
|
Backend: v.GetString(GPUBackendKey),
|
|
DeviceIndex: v.GetInt(GPUDeviceKey),
|
|
LogLevel: v.GetString(GPULogLevelKey),
|
|
}
|
|
if err := SetGlobalGPUConfig(gpuConfig); err != nil {
|
|
return node.Config{}, fmt.Errorf("invalid GPU configuration: %w", err)
|
|
}
|
|
|
|
nodeConfig.ConsensusShutdownTimeout = v.GetDuration(ConsensusShutdownTimeoutKey)
|
|
if nodeConfig.ConsensusShutdownTimeout < 0 {
|
|
return node.Config{}, fmt.Errorf("%q must be >= 0", ConsensusShutdownTimeoutKey)
|
|
}
|
|
|
|
// Gossiping
|
|
nodeConfig.FrontierPollFrequency = v.GetDuration(ConsensusFrontierPollFrequencyKey)
|
|
if nodeConfig.FrontierPollFrequency < 0 {
|
|
return node.Config{}, fmt.Errorf("%s must be >= 0", ConsensusFrontierPollFrequencyKey)
|
|
}
|
|
|
|
// App handling
|
|
nodeConfig.ConsensusAppConcurrency = int(v.GetUint(ConsensusAppConcurrencyKey))
|
|
if nodeConfig.ConsensusAppConcurrency <= 0 {
|
|
return node.Config{}, fmt.Errorf("%s must be > 0", ConsensusAppConcurrencyKey)
|
|
}
|
|
|
|
nodeConfig.UseCurrentHeight = v.GetBool(ProposerVMUseCurrentHeightKey)
|
|
|
|
// Logging
|
|
// nodeConfig.LoggingConfig, err = getLoggingConfig(v)
|
|
// if err != nil {
|
|
// return node.Config{}, err
|
|
// }
|
|
//
|
|
// Network ID - use network name (shorthand flags removed)
|
|
// if v.GetBool(MainnetKey) {
|
|
// nodeConfig.NetworkID = constants.MainnetChainID
|
|
// } else if v.GetBool(TestnetKey) {
|
|
// nodeConfig.NetworkID = constants.TestnetChainID
|
|
// } else if v.GetBool(CustomnetKey) {
|
|
// nodeConfig.NetworkID = constants.LocalID
|
|
// } else {
|
|
networkName := v.GetString(NetworkNameKey)
|
|
nodeConfig.NetworkID, err = constants.NetworkID(networkName)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
// }
|
|
|
|
// Database
|
|
nodeConfig.DatabaseConfig, err = getDatabaseConfig(v, nodeConfig.NetworkID)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
|
|
// IP configuration
|
|
nodeConfig.IPConfig, err = getIPConfig(v)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
|
|
// Staking
|
|
nodeConfig.StakingConfig, err = getStakingConfig(v, nodeConfig.NetworkID)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
|
|
// Chain tracking — explicit per-chain opt-in. Each validator
|
|
// declares which chains it wants to validate via --track-chains
|
|
// (or the operator-emitted equivalent). TrackedChains is also used
|
|
// in the peer handshake (MyChains) so peers gossip blocks for the
|
|
// chains we asked for.
|
|
//
|
|
// TrackAllChains stays as an opt-in escape hatch only — it is NOT
|
|
// auto-enabled when TrackedChains is empty. The previous default
|
|
// (`if TrackedChains == nil { TrackAllChains = true }`) was a
|
|
// footgun: any node started without an explicit --track-chains
|
|
// list silently tried to spin up every chain in P-chain state,
|
|
// including ones whose VM plugin wasn't loaded — fataling at the
|
|
// chain manager. Empty TrackedChains now means "track only P/X
|
|
// (built-in primary chains)".
|
|
nodeConfig.TrackedChains, err = getTrackedChains(v)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
nodeConfig.TrackAllChains = v.GetBool(TrackAllChainsKey)
|
|
nodeConfig.DexValidator = v.GetBool(DexValidatorKey)
|
|
|
|
// HTTP APIs
|
|
nodeConfig.HTTPConfig, err = getHTTPConfig(v)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
|
|
// Health
|
|
nodeConfig.HealthCheckFreq = v.GetDuration(HealthCheckFreqKey)
|
|
if nodeConfig.HealthCheckFreq < 0 {
|
|
return node.Config{}, fmt.Errorf("%s must be positive", HealthCheckFreqKey)
|
|
}
|
|
nodeConfig.ProposerWindowDuration = v.GetDuration(ProposerVMWindowDurationKey)
|
|
if nodeConfig.ProposerWindowDuration < 0 {
|
|
return node.Config{}, fmt.Errorf("%s must not be negative", ProposerVMWindowDurationKey)
|
|
}
|
|
nodeConfig.ProposerMinBlockDelay = v.GetDuration(ProposerVMMinBlockDelayKey)
|
|
if nodeConfig.ProposerMinBlockDelay < 0 {
|
|
return node.Config{}, fmt.Errorf("%s must not be negative", ProposerVMMinBlockDelayKey)
|
|
}
|
|
// Halflife of continuous averager used in health checks
|
|
healthCheckAveragerHalflife := v.GetDuration(HealthCheckAveragerHalflifeKey)
|
|
if healthCheckAveragerHalflife <= 0 {
|
|
return node.Config{}, fmt.Errorf("%s must be positive", HealthCheckAveragerHalflifeKey)
|
|
}
|
|
|
|
// Router
|
|
// routerHealthCfg, err := getRouterHealthConfig(v, healthCheckAveragerHalflife)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
// // Convert RouterHealthConfig to node.HealthConfig
|
|
// nodeConfig.RouterHealthConfig = node.HealthConfig{
|
|
// MaxTimeSinceMsgReceived: routerHealthCfg.MaxOutstandingDuration,
|
|
// MaxTimeSinceMsgSent: routerHealthCfg.MaxOutstandingDuration,
|
|
// MaxPortionSendQueueFull: routerHealthCfg.MaxDropRate,
|
|
// MinConnectedPeers: 1,
|
|
// ReadTimeout: routerHealthCfg.MaxRunTimeRequests,
|
|
// WriteTimeout: routerHealthCfg.MaxRunTimeRequests,
|
|
// MaxSendFailRate: routerHealthCfg.MaxDropRate,
|
|
// }
|
|
|
|
// Metrics
|
|
nodeConfig.MeterVMEnabled = v.GetBool(MeterVMsEnabledKey)
|
|
|
|
// Adaptive Timeout Config
|
|
nodeConfig.AdaptiveTimeoutConfig, err = getAdaptiveTimeoutConfig(v)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
|
|
// Upgrade config
|
|
nodeConfig.UpgradeConfig, err = getUpgradeConfig(v, nodeConfig.NetworkID)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
|
|
// Network Config
|
|
nodeConfig.NetworkConfig, err = getNetworkConfig(
|
|
v,
|
|
nodeConfig.NetworkID,
|
|
nodeConfig.SybilProtectionEnabled,
|
|
healthCheckAveragerHalflife,
|
|
)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
|
|
// Net Configs
|
|
chainConfigs, err := getNetConfigs(v, nodeConfig.TrackedChains.List())
|
|
if err != nil {
|
|
return node.Config{}, fmt.Errorf("couldn't read net configs: %w", err)
|
|
}
|
|
|
|
primaryNetworkConfig := getDefaultNetConfig(v)
|
|
if err := primaryNetworkConfig.Valid(); err != nil {
|
|
return node.Config{}, fmt.Errorf("invalid consensus parameters: %w", err)
|
|
}
|
|
chainConfigs[constants.PrimaryNetworkID] = primaryNetworkConfig
|
|
|
|
nodeConfig.NetConfigs = chainConfigs
|
|
|
|
// Benchlist
|
|
// Convert consensus.Parameters to PrismParameters for benchlist config
|
|
// prismParams := PrismParameters{
|
|
// K: primaryNetworkConfig.ConsensusParameters.K,
|
|
// AlphaPreference: primaryNetworkConfig.ConsensusParameters.AlphaPreference,
|
|
// AlphaConfidence: primaryNetworkConfig.ConsensusParameters.AlphaConfidence,
|
|
// }
|
|
// getBenchlistConfig is called for validation only
|
|
// _, err = getBenchlistConfig(v, prismParams)
|
|
// if err != nil {
|
|
// return node.Config{}, err
|
|
// }
|
|
// benchlist.Config from consensus package only has Deprecated field
|
|
nodeConfig.BenchlistConfig = benchlist.Config{
|
|
Deprecated: false,
|
|
}
|
|
|
|
// File Descriptor Limit
|
|
nodeConfig.FdLimit = v.GetUint64(FdLimitKey)
|
|
|
|
// Tx Fee
|
|
nodeConfig.TxFeeConfig = getTxFeeConfig(v, nodeConfig.NetworkID)
|
|
|
|
// Genesis Data
|
|
genesisStakingCfg := nodeConfig.StakingConfig.StakingConfig
|
|
|
|
// Add BLS key information for genesis replay
|
|
if nodeConfig.StakingConfig.StakingSigningKey != nil {
|
|
// Get NodeID from the certificate
|
|
nodeID := ids.NodeIDFromCert(&ids.Certificate{
|
|
Raw: nodeConfig.StakingConfig.StakingTLSCert.Leaf.Raw,
|
|
PublicKey: nodeConfig.StakingConfig.StakingTLSCert.Leaf.PublicKey,
|
|
})
|
|
genesisStakingCfg.NodeID = nodeID.String()
|
|
|
|
// Get BLS public key and proof of possession
|
|
pk := nodeConfig.StakingConfig.StakingSigningKey.PublicKey()
|
|
genesisStakingCfg.BLSPublicKey = bls.PublicKeyToCompressedBytes(pk)
|
|
|
|
// Generate proof of possession
|
|
sig, err := nodeConfig.StakingConfig.StakingSigningKey.SignProofOfPossession(genesisStakingCfg.BLSPublicKey)
|
|
if err != nil {
|
|
return node.Config{}, fmt.Errorf("failed to generate BLS proof of possession: %w", err)
|
|
}
|
|
if sig != nil {
|
|
genesisStakingCfg.BLSProofOfPossession = bls.SignatureToBytes(sig)
|
|
}
|
|
}
|
|
|
|
// Get data directory for dev network config persistence
|
|
dataDir := getExpandedArg(v, DataDirKey)
|
|
|
|
nodeConfig.GenesisBytes, nodeConfig.UTXOAssetID, err = getGenesisData(v, nodeConfig.NetworkID, &genesisStakingCfg, dataDir)
|
|
if err != nil {
|
|
return node.Config{}, fmt.Errorf("unable to load genesis file: %w", err)
|
|
}
|
|
|
|
// StateSync Configs
|
|
nodeConfig.StateSyncConfig, err = getStateSyncConfig(v)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
|
|
// Bootstrap Configs
|
|
nodeConfig.BootstrapConfig, err = getBootstrapConfig(v, nodeConfig.NetworkID)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
|
|
// Chain Configs
|
|
nodeConfig.ChainConfigs, err = getChainConfigs(v)
|
|
if err != nil {
|
|
return node.Config{}, fmt.Errorf("couldn't read chain configs: %w", err)
|
|
}
|
|
|
|
// Bridge --import-chain-data into the C-Chain config so the EVM plugin sees it.
|
|
// Merges with any existing C-chain config rather than replacing.
|
|
if importPath := v.GetString(ImportChainDataKey); importPath != "" {
|
|
cChain := nodeConfig.ChainConfigs["C"]
|
|
merged := map[string]interface{}{}
|
|
if len(cChain.Config) > 0 {
|
|
if err := json.Unmarshal(cChain.Config, &merged); err != nil {
|
|
return node.Config{}, fmt.Errorf("couldn't parse existing C-chain config for import bridge: %w", err)
|
|
}
|
|
}
|
|
merged["import-chain-data"] = importPath
|
|
mergedBytes, err := json.Marshal(merged)
|
|
if err != nil {
|
|
return node.Config{}, fmt.Errorf("couldn't marshal C-chain config with import path: %w", err)
|
|
}
|
|
cChain.Config = mergedBytes
|
|
nodeConfig.ChainConfigs["C"] = cChain
|
|
}
|
|
|
|
// Profiler
|
|
nodeConfig.ProfilerConfig, err = getProfilerConfig(v)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
|
|
// VM Aliases
|
|
nodeConfig.VMAliases, err = getVMAliases(v)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
// Chain aliases
|
|
nodeConfig.ChainAliases, err = getChainAliases(v)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
|
|
nodeConfig.SystemTrackerFrequency = v.GetDuration(SystemTrackerFrequencyKey)
|
|
nodeConfig.SystemTrackerProcessingHalflife = v.GetDuration(SystemTrackerProcessingHalflifeKey)
|
|
nodeConfig.SystemTrackerCPUHalflife = v.GetDuration(SystemTrackerCPUHalflifeKey)
|
|
nodeConfig.SystemTrackerDiskHalflife = v.GetDuration(SystemTrackerDiskHalflifeKey)
|
|
|
|
nodeConfig.RequiredAvailableDiskSpace, nodeConfig.WarningThresholdAvailableDiskSpace, err = getDiskSpaceConfig(v)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
|
|
// cpuTargeterCfg, err := getCPUTargeterConfig(v)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
// Convert TrackerTargeterConfig to node.TargeterConfig
|
|
// nodeConfig.CPUTargeterConfig = node.TargeterConfig{
|
|
// VdrAlloc: cpuTargeterCfg.VdrAlloc,
|
|
// MaxNonVdrUsage: cpuTargeterCfg.MaxNonVdrUsage,
|
|
// MaxNonVdrNodeUsage: cpuTargeterCfg.MaxNonVdrNodeUsage,
|
|
// }
|
|
|
|
// diskTargeterCfg, err := getDiskTargeterConfig(v)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
// Convert TrackerTargeterConfig to node.TargeterConfig
|
|
// nodeConfig.DiskTargeterConfig = node.TargeterConfig{
|
|
// VdrAlloc: diskTargeterCfg.VdrAlloc,
|
|
// MaxNonVdrUsage: diskTargeterCfg.MaxNonVdrUsage,
|
|
// MaxNonVdrNodeUsage: diskTargeterCfg.MaxNonVdrNodeUsage,
|
|
// }
|
|
|
|
nodeConfig.TraceConfig, err = getTraceConfig(v)
|
|
if err != nil {
|
|
return node.Config{}, err
|
|
}
|
|
|
|
nodeConfig.ChainDataDir = getExpandedArg(v, ChainDataDirKey)
|
|
|
|
nodeConfig.ProcessContextFilePath = getExpandedArg(v, ProcessContextFileKey)
|
|
|
|
nodeConfig.ProvidedFlags = providedFlags(v)
|
|
|
|
// Low Memory Configuration
|
|
lowMemConfig := GetLowMemoryConfig(v)
|
|
nodeConfig.LowMemoryEnabled = lowMemConfig.Enabled
|
|
nodeConfig.DBCacheSize = lowMemConfig.DBCacheSize
|
|
nodeConfig.DBMemtableSize = lowMemConfig.DBMemtableSize
|
|
nodeConfig.StateCacheSize = lowMemConfig.StateCacheSize
|
|
nodeConfig.BlockCacheSize = lowMemConfig.BlockCacheSize
|
|
nodeConfig.DisableBloomFilters = lowMemConfig.DisableBloomFilters
|
|
nodeConfig.LazyChainLoading = lowMemConfig.LazyChainLoading
|
|
nodeConfig.SingleValidatorMode = lowMemConfig.SingleValidatorMode
|
|
|
|
// Initialize logger if not already set
|
|
// if nodeConfig.Log == nil {
|
|
// nodeConfig.Log = log.New()
|
|
// }
|
|
|
|
return nodeConfig, nil
|
|
}
|
|
|
|
// loadCachedGenesisBytes loads cached platform genesis bytes from a file.
|
|
// Returns the bytes if found, or an error if not found/unreadable.
|
|
func loadCachedGenesisBytes(cacheFile string) ([]byte, error) {
|
|
return os.ReadFile(cacheFile)
|
|
}
|
|
|
|
// saveCachedGenesisBytes saves platform genesis bytes to a cache file.
|
|
func saveCachedGenesisBytes(cacheFile string, genesisBytes []byte) error {
|
|
return os.WriteFile(cacheFile, genesisBytes, 0o600)
|
|
}
|
|
|
|
func providedFlags(v *viper.Viper) map[string]interface{} {
|
|
settings := v.AllSettings()
|
|
customSettings := make(map[string]interface{}, len(settings))
|
|
for key, val := range settings {
|
|
if v.IsSet(key) {
|
|
customSettings[key] = val
|
|
}
|
|
}
|
|
return customSettings
|
|
}
|