node: refresh config + rpcdb + vm registry + subprocess initializer

Routine maintenance: config.go, rpcdb/service.go, node/node.go,
vms/registry/registry.go, vms/rpcchainvm/runtime/subprocess/initializer.go.
This commit is contained in:
Hanzo AI
2026-05-20 16:26:18 -07:00
parent f471f9e98a
commit 8d461c661e
5 changed files with 25 additions and 388 deletions
+14 -342
View File
@@ -26,7 +26,6 @@ import (
"github.com/luxfi/constants"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/bls/signer/localsigner"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/crypto/mldsa"
mlkemcrypto "github.com/luxfi/crypto/mlkem"
"github.com/luxfi/filesystem/perms"
@@ -99,90 +98,16 @@ var (
errFileDoesNotExist = errors.New("file does not exist")
)
// AutomineNetworkConfig captures immutable network state for automine mode persistence.
// Once written to automine-network.json, this ensures the same genesis is produced
// on every restart, making C-Chain/EVM state persistence work correctly.
type AutomineNetworkConfig struct {
// Version for forward compatibility
Version int `json:"version"`
// Genesis start time - captured on first boot, reused on restart
StartTime uint64 `json:"startTime"`
// Node identity
NodeID string `json:"nodeId"`
// BLS credentials (hex-encoded)
BLSPublicKey string `json:"blsPublicKey"`
BLSPopProof string `json:"blsPopProof"`
// Computed genesis bytes and hash (for verification)
GenesisBytes []byte `json:"genesisBytes"`
GenesisHash string `json:"genesisHash"` // Actual hash of GenesisBytes
// X-Chain asset ID (LUX token ID)
XAssetID string `json:"xAssetId"`
// C-Chain genesis (stored separately for EVM immutability)
CChainGenesis string `json:"cChainGenesis"`
}
const (
devNetworkConfigVersion = 1
devNetworkConfigFilename = "automine-network.json"
)
// loadAutomineNetworkConfig attempts to load automine-network.json from the data directory.
// Returns nil if the file doesn't exist (first boot scenario).
func loadAutomineNetworkConfig(dataDir string) (*AutomineNetworkConfig, error) {
path := filepath.Join(dataDir, devNetworkConfigFilename)
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil // First boot - no config yet
}
return nil, fmt.Errorf("failed to read dev network config: %w", err)
}
var cfg AutomineNetworkConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse dev network config: %w", err)
}
if cfg.Version != devNetworkConfigVersion {
return nil, fmt.Errorf("unsupported dev network config version: %d (expected %d)", cfg.Version, devNetworkConfigVersion)
}
return &cfg, nil
}
// saveAutomineNetworkConfig atomically writes the dev network config to disk.
// Uses write-to-temp-then-rename pattern for crash safety.
func saveAutomineNetworkConfig(dataDir string, cfg *AutomineNetworkConfig) error {
path := filepath.Join(dataDir, devNetworkConfigFilename)
tempPath := path + ".tmp"
cfg.Version = devNetworkConfigVersion
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal dev network config: %w", err)
}
// Write to temp file
if err := os.WriteFile(tempPath, data, 0o600); err != nil {
return fmt.Errorf("failed to write temp dev network config: %w", err)
}
// Atomic rename
if err := os.Rename(tempPath, path); err != nil {
os.Remove(tempPath) // Clean up on failure
return fmt.Errorf("failed to rename dev network config: %w", err)
}
return nil
}
// (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
@@ -1115,10 +1040,10 @@ func getUpgradeConfig(v *viper.Viper, networkID uint32) (upgrade.Config, error)
}
func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.StakingConfig, dataDir string) ([]byte, ids.ID, error) {
// Handle automine mode genesis - dynamically generate genesis with the node's own credentials
if v.GetBool(DevModeKey) && !v.IsSet(GenesisFileKey) && !v.IsSet(GenesisFileContentKey) && !v.IsSet(GenesisDBKey) {
return getOrCreateAutomineGenesis(stakingCfg, dataDir)
}
// (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
@@ -1205,259 +1130,6 @@ func getGenesisData(v *viper.Viper, networkID uint32, stakingCfg *builder.Stakin
return builder.FromConfig(config)
}
// getOrCreateAutomineGenesis handles automine mode genesis with persistence.
// On first boot: generates genesis, saves to automine-network.json, returns genesis.
// On restart: loads from automine-network.json to ensure genesis hash stability.
// Returns: (genesisBytes, xAssetID, error) - xAssetID is the LUX token asset ID
func getOrCreateAutomineGenesis(stakingCfg *builder.StakingConfig, dataDir string) ([]byte, ids.ID, error) {
// Try to load existing dev network config
devCfg, err := loadAutomineNetworkConfig(dataDir)
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to load dev network config: %w", err)
}
if devCfg != nil {
// Existing config found - check if credentials match
// In automine mode with ephemeral certs, credentials will change on restart.
// We log a warning but continue with the stored genesis since automine mode
// is single-node and doesn't require credential consistency.
expectedNodeID := stakingCfg.NodeID
expectedBLSPK := fmt.Sprintf("0x%x", stakingCfg.BLSPublicKey)
expectedBLSPoP := fmt.Sprintf("0x%x", stakingCfg.BLSProofOfPossession)
credentialsMismatch := false
if devCfg.NodeID != expectedNodeID {
log.Warn("automine-network.json nodeID mismatch (using stored genesis anyway for automine mode)",
"stored", devCfg.NodeID,
"current", expectedNodeID,
)
credentialsMismatch = true
}
if devCfg.BLSPublicKey != expectedBLSPK {
log.Warn("automine-network.json BLS public key mismatch (using stored genesis anyway for automine mode)")
credentialsMismatch = true
}
if devCfg.BLSPopProof != expectedBLSPoP {
log.Warn("automine-network.json BLS PoP mismatch (using stored genesis anyway for automine mode)")
credentialsMismatch = true
}
if credentialsMismatch {
log.Warn("credentials changed since first boot - for persistent staking, use --staking-ephemeral-cert-enabled=false with persistent key files")
}
// Verify the stored genesis hash matches what we'll compute from the bytes
storedHash, err := ids.FromString(devCfg.GenesisHash)
if err != nil {
return nil, ids.Empty, fmt.Errorf("invalid genesis hash in automine-network.json: %w", err)
}
// Compute actual hash from stored bytes to verify integrity
computedHashBytes := hash.ComputeHash256(devCfg.GenesisBytes)
computedHash, err := ids.ToID(computedHashBytes)
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to convert computed hash to ID: %w", err)
}
if storedHash != computedHash {
return nil, ids.Empty, fmt.Errorf("genesis bytes corrupted: stored hash %s != computed hash %s", storedHash, computedHash)
}
// Parse stored X-Chain asset ID
xAssetID, err := ids.FromString(devCfg.XAssetID)
if err != nil {
return nil, ids.Empty, fmt.Errorf("invalid xAssetId in automine-network.json: %w", err)
}
log.Info("loaded dev network config",
"path", filepath.Join(dataDir, devNetworkConfigFilename),
"genesisHash", devCfg.GenesisHash,
"xAssetID", devCfg.XAssetID,
"startTime", devCfg.StartTime,
)
return devCfg.GenesisBytes, xAssetID, nil
}
// First boot - generate new genesis with current timestamp
startTime := uint64(time.Now().Unix())
// buildAutomineGenesis returns (genesisBytes, xAssetID) - the LUX token asset ID
genesisBytes, xAssetID, err := buildAutomineGenesis(stakingCfg, startTime)
if err != nil {
return nil, ids.Empty, err
}
// Compute actual genesis hash from bytes (this is what the node uses for DB validation)
genesisHashBytes := hash.ComputeHash256(genesisBytes)
genesisHash, err := ids.ToID(genesisHashBytes)
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to convert genesis hash to ID: %w", err)
}
// Save for future restarts. The CChainGenesis field is metadata only —
// the actual genesis bytes are embedded in genesisBytes — but mirror
// the resolved value so audits read true.
savedCChain := automineCChainGenesis
newDevCfg := &AutomineNetworkConfig{
Version: devNetworkConfigVersion,
StartTime: startTime,
NodeID: stakingCfg.NodeID,
BLSPublicKey: fmt.Sprintf("0x%x", stakingCfg.BLSPublicKey),
BLSPopProof: fmt.Sprintf("0x%x", stakingCfg.BLSProofOfPossession),
GenesisBytes: genesisBytes,
GenesisHash: genesisHash.String(),
XAssetID: xAssetID.String(),
CChainGenesis: savedCChain,
}
if err := saveAutomineNetworkConfig(dataDir, newDevCfg); err != nil {
// Log warning but don't fail - genesis is still valid
log.Warn("failed to save dev network config (persistence may not work on restart)",
"error", err,
)
} else {
log.Info("created dev network config",
"path", filepath.Join(dataDir, devNetworkConfigFilename),
"genesisHash", genesisHash.String(),
"xAssetID", xAssetID.String(),
"startTime", startTime,
)
}
return genesisBytes, xAssetID, nil
}
// buildAutomineGenesis creates a genesis configuration for single-node development mode.
// It uses the node's own credentials as the sole validator.
//
// C-Chain genesis comes from the embedded automineCChainGenesis constant
// below (chainId 31337). Downstream networks that need a different
// chainId ship their own platform-genesis bundle; the node does not
// accept an out-of-band file path at runtime.
func buildAutomineGenesis(stakingCfg *builder.StakingConfig, startTime uint64) ([]byte, ids.ID, error) {
// Parse node ID from staking config
nodeID, err := ids.NodeIDFromString(stakingCfg.NodeID)
if err != nil {
return nil, ids.Empty, fmt.Errorf("failed to parse node ID for automine mode: %w", err)
}
// C-Chain genesis: built-in default (downstream networks bring
// their own platform-genesis bundle, not a path read at boot).
cChainGenesis := automineCChainGenesis
// Lux Treasury address: 0x9011E888251AB053B7bD1cdB598Db4f9DEd94714
// This is derived from the MNEMONIC env var and funded on C-Chain
var rewardAddress ids.ShortID
treasuryAddr := "9011E888251AB053B7bD1cdB598Db4f9DEd94714"
treasuryBytes, err := ids.ShortFromString(treasuryAddr)
if err == nil {
rewardAddress = treasuryBytes
} else {
// Fall back to a deterministic address derived from node ID
copy(rewardAddress[:], nodeID[:20])
}
// Create automine mode config with embedded C-Chain genesis (resolved
// above — operator override or built-in default).
devCfg := builder.DevModeConfig{
NodeID: nodeID,
BLSPublicKey: fmt.Sprintf("0x%x", stakingCfg.BLSPublicKey),
BLSPopProof: fmt.Sprintf("0x%x", stakingCfg.BLSProofOfPossession),
RewardAddress: rewardAddress,
CChainGenesis: cChainGenesis,
StartTime: startTime, // Use provided start time for determinism
}
return builder.ForDevMode(devCfg, stakingCfg)
}
// automineCChainGenesis is the default C-Chain genesis for automine mode.
// Network ID 1337, EVM Chain ID 31337.
// Funds Lux Treasury (0x9011), light mnemonic accounts (BIP44 m/44'/9000'/0'/0/{0-4}), and Anvil/Hardhat accounts.
const automineCChainGenesis = `{
"config": {
"chainId": 31337,
"homesteadBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"muirGlacierBlock": 0,
"berlinBlock": 0,
"londonBlock": 0,
"arrowGlacierBlock": 0,
"grayGlacierBlock": 0,
"mergeNetsplitBlock": 0,
"shanghaiTime": 0,
"cancunTime": 0,
"blobSchedule": {
"cancun": {"target": 3, "max": 6, "baseFeeUpdateFraction": 3338477}
},
"terminalTotalDifficulty": 0,
"chainEVMTimestamp": 0,
"feeConfig": {
"gasLimit": 30000000,
"targetBlockRate": 1,
"minBaseFee": 1000000000,
"targetGas": 100000000,
"baseFeeChangeDenominator": 48,
"minBlockGasCost": 0,
"maxBlockGasCost": 1000000,
"blockGasCostStep": 200000
},
"warpConfig": {
"blockTimestamp": 0,
"quorumNumerator": 67,
"requirePrimaryNetworkSigners": false
}
},
"alloc": {
"9011E888251AB053B7bD1cdB598Db4f9DEd94714": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"5369615110ca435bdf798f31c20ba6163d7b0a54": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"2e701063ccdffa2b1872c596222d8067d124d3ef": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"9030463eb1aaa563c8247468416cc0bf06347502": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"f77b06331152fd0e536de0af65688a6559c6f914": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"944fd51713652b9922690b7d06498fbf8742beac": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"f39Fd6e51aad88F6F4ce6aB8827279cffFb92266": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"70997970C51812dc3A010C7d01b50e0d17dc79C8": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"3C44CdDdB6a900fa2b585dd299e03d12FA4293BC": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
},
"90F79bf6EB2c4f870365E785982E1f101E93b906": {
"balance": "0x200000000000000000000000000000000000000000000000000000000000000"
}
},
"nonce": "0x0",
"timestamp": "0x0",
"extraData": "0x",
"gasLimit": "0x1c9c380",
"difficulty": "0x0",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x0000000000000000000000000000000000000000",
"number": "0x0",
"gasUsed": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000"
}`
func getTrackedChains(v *viper.Viper) (set.Set[ids.ID], error) {
trackChainsStr := v.GetString(TrackChainsKey)
+1 -1
View File
@@ -8,7 +8,7 @@
// Layered topology:
//
// Layer A — wire framing (github.com/luxfi/api/zap)
// Layer B — service spec (data carriers) (github.com/luxfi/protocol/rpcdb)
// Layer B — service spec (data carriers) (github.com/luxfi/proto/rpcdb)
// Layer C — service impl + transports (this package)
//
// One Service. Many transport adapters. Adding a new transport is a
+1 -2
View File
@@ -1542,9 +1542,8 @@ func (n *Node) initVMs() error {
"error", err,
)
}
// Don't fail if Reload returns an error - it might be due to already registered VMs
if err != nil {
n.Log.Warn("VM registry reload encountered issues, continuing anyway", "error", err)
return fmt.Errorf("reload VM registry: %w", err)
}
return nil
}
+9 -28
View File
@@ -80,75 +80,56 @@ func NewVMGetter(config VMGetterConfig) VMGetter {
}
}
// Reload scans the plugin directory and registers any VM that isn't yet
// known to the manager. Files whose name doesn't parse as an ids.ID are
// skipped silently; a VM that is already registered is skipped without
// error. Per-VM load failures are collected into failedVMs so the caller
// can report them. Only fundamental issues (unreadable plugin dir) bubble
// up as the third return value.
func (r *vmRegistry) Reload(ctx context.Context) ([]ids.ID, map[ids.ID]error, error) {
var newVMs []ids.ID
failedVMs := make(map[ids.ID]error)
getter, ok := r.config.VMGetter.(*vmGetter)
if !ok {
fmt.Printf("[Registry] VMGetter type assertion failed\n")
return newVMs, failedVMs, nil
}
pluginDir := getter.config.PluginDirectory
if pluginDir == "" {
fmt.Printf("[Registry] No plugin directory configured\n")
return newVMs, failedVMs, nil
}
fmt.Printf("[Registry] Loading plugins from: %s\n", pluginDir)
files, err := os.ReadDir(pluginDir)
if err != nil {
if os.IsNotExist(err) {
fmt.Printf("[Registry] Plugin directory does not exist: %s\n", pluginDir)
return newVMs, failedVMs, nil
}
return nil, nil, fmt.Errorf("failed to read plugin directory: %w", err)
return nil, nil, fmt.Errorf("read plugin directory %s: %w", pluginDir, err)
}
fmt.Printf("[Registry] Found %d files in plugin directory\n", len(files))
for _, file := range files {
if file.IsDir() {
fmt.Printf("[Registry] Skipping directory: %s\n", file.Name())
continue
}
pluginPath := filepath.Join(pluginDir, file.Name())
fmt.Printf("[Registry] Checking file: %s\n", file.Name())
vmID, err := ids.FromString(file.Name())
if err != nil {
// Skip files that aren't valid VM IDs
fmt.Printf("[Registry] Invalid VM ID format: %s (err: %v)\n", file.Name(), err)
continue
}
fmt.Printf("[Registry] Parsed VM ID: %s\n", vmID)
// Check if already registered
fmt.Printf("[Registry] Checking if VM %s is already registered...\n", vmID)
existingFactory, getErr := r.config.VMManager.GetFactory(ctx, vmID)
fmt.Printf("[Registry] GetFactory returned: factory=%v, err=%v\n", existingFactory, getErr)
if getErr == nil {
// Already registered
fmt.Printf("[Registry] VM already registered: %s\n", vmID)
if _, getErr := r.config.VMManager.GetFactory(ctx, vmID); getErr == nil {
continue
}
fmt.Printf("[Registry] VM not registered, will load from plugin\n")
fmt.Printf("[Registry] Loading factory from plugin: %s\n", pluginPath)
factory, err := r.config.VMGetter.Get(pluginPath)
factory, err := r.config.VMGetter.Get(filepath.Join(pluginDir, file.Name()))
if err != nil {
failedVMs[vmID] = err
continue
}
if err := r.config.VMManager.RegisterFactory(ctx, vmID, factory); err != nil {
failedVMs[vmID] = err
continue
}
newVMs = append(newVMs, vmID)
}
return newVMs, failedVMs, nil
}
@@ -6,7 +6,6 @@ package subprocess
import (
"context"
"fmt"
"os"
"sync"
"github.com/luxfi/node/version"
@@ -36,17 +35,7 @@ func newInitializer(path string) *initializer {
}
func (i *initializer) Initialize(_ context.Context, protocolVersion uint, vmAddr string) error {
// File-based debug logging
debugFile := func(msg string) {
if f, err := os.OpenFile("/tmp/node_initializer.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil {
fmt.Fprintf(f, "[node] %s\n", msg)
f.Close()
}
}
debugFile(fmt.Sprintf("Initialize called: protocolVersion=%d, vmAddr=%s, path=%s", protocolVersion, vmAddr, i.path))
i.once.Do(func() {
debugFile(fmt.Sprintf("once.Do executing: nodeProtocol=%d, vmProtocol=%d", version.RPCChainVMProtocol, protocolVersion))
if version.RPCChainVMProtocol != protocolVersion {
i.err = fmt.Errorf("%w. Lux Node version %s implements RPCChainVM protocol version %d. The VM located at %s implements RPCChainVM protocol version %d. Please make sure that there is an exact match of the protocol versions. This can be achieved by updating your VM or running an older/newer version of Lux Node. Please be advised that some virtual machines may not yet support the latest RPCChainVM protocol version",
runtime.ErrProtocolVersionMismatch,
@@ -55,13 +44,9 @@ func (i *initializer) Initialize(_ context.Context, protocolVersion uint, vmAddr
i.path,
protocolVersion,
)
debugFile("Protocol mismatch error: " + i.err.Error())
}
i.vmAddr = vmAddr
debugFile("Closing initialized channel")
close(i.initialized)
debugFile("Initialized channel closed")
})
debugFile(fmt.Sprintf("Returning from Initialize, err=%v", i.err))
return i.err
}