Files
node/cmd/config/main.go
T
Hanzo AI c3b398bc7b json: migrate every encoding/json import to json/v2 (go-json-experiment)
External (HTTP / JSON-RPC) is the only place JSON is legitimate. Every
existing encoding/json import in node/ moves to github.com/go-json-experiment/json
(v2 root, not the v1 sub-package). NewEncoder/NewDecoder rewrite to
MarshalWrite/UnmarshalRead. MarshalIndent rewrites to Marshal with
jsontext.WithIndent. json.RawMessage rewrites to jsontext.Value.
*json.SyntaxError rewrites to *jsontext.SyntacticError.

81 files migrated. LLM.md captures the rule + v1->v2 delta table.

Known v2 semantic deltas surfaced by existing tests (followups, not regressions):
- [N]byte fields with no MarshalJSON now marshal as base64 string (v1 marshalled
  as JSON array of byte numbers). Affects vms/platformvm/txs/*_test.go fixtures
  with embedded BLS proofOfPossession.
- time.Duration has no v2 default representation; configs that wire-format
  Duration as nanoseconds (vms/{xvm,platformvm}/config, config/spec) need to
  switch to string-form Duration or carry an explicit option. v2 root does not
  re-export FormatDurationAsNano.
- v2 enforces strict UTF-8 (vms/chainadapter/messaging fixture has non-UTF-8).
- json.MarshalWrite does not append a trailing '\n' (v1 NewEncoder.Encode did);
  service/auth/auth_test.go expectation updated.
- nil []byte round-trips to empty (not nil); config_test deep-equal fixtures
  surface this.

All affected sites are at the API boundary; ZAP wire envelope already covers
the internal data paths (state, P2P, consensus, MPC, threshold). Internal
JSON sites that should move to ZAP next (separate work):
- vms/da/store.go            (DA blob/cert storage as JSON)
- vms/platformvm/airdrop     (airdrop claims as JSON in db)
- vms/chainadapter/appchain  (SQLite materializer schema/data blobs)
- vms/chainadapter/messaging (conversation codec)
- staking/kms.go             (KMS HTTP client — external technically, leave)
- utils/{bimap,ips}          (small marshaler shims — low priority)
2026-06-06 22:26:02 -07:00

366 lines
7.8 KiB
Go

// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Command config provides utilities for working with luxd configuration.
//
// Usage:
//
// luxd-config spec [--format=json|md|go]
// luxd-config validate <config-file>
// luxd-config diff <config-file>
package main
import (
"github.com/go-json-experiment/json"
"fmt"
"os"
"sort"
"strings"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/luxfi/node/config/spec"
)
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
cmd := os.Args[1]
args := os.Args[2:]
switch cmd {
case "spec":
dumpSpec(args)
case "validate":
validate(args)
case "diff":
showDiff(args)
case "help", "-h", "--help":
printUsage()
default:
fmt.Printf("Unknown command: %s\n", cmd)
printUsage()
os.Exit(1)
}
}
func printUsage() {
fmt.Println(`luxd-config - Lux Node Configuration Tool
Usage:
luxd-config <command> [options]
Commands:
spec Output the complete configuration specification
validate Validate a configuration file
diff Show difference between config file and defaults
Options for spec:
--format=json Output as JSON (default)
--format=md Output as Markdown documentation
--format=go Output as Go code for embedding
Options for validate:
--strict Fail on unknown keys (default: warn)
--quiet Only output errors
Examples:
luxd-config spec --format=json > spec.json
luxd-config validate /path/to/config.json
luxd-config diff /path/to/config.json`)
}
func dumpSpec(args []string) {
fs := pflag.NewFlagSet("spec", pflag.ExitOnError)
format := fs.String("format", "json", "Output format: json, md, or go")
if err := fs.Parse(args); err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
s := spec.Spec()
switch *format {
case "json":
dumpJSON(s)
case "md":
dumpMarkdown(s)
case "go":
dumpGo(s)
default:
fmt.Printf("Unknown format: %s\n", *format)
os.Exit(1)
}
}
func dumpJSON(s *spec.ConfigSpec) {
data, err := s.JSON()
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
fmt.Println(string(data))
}
func dumpMarkdown(s *spec.ConfigSpec) {
fmt.Printf("# Lux Node Configuration Reference\n\n")
fmt.Printf("Version: %s\n", s.Version)
fmt.Printf("Node Version: %s\n", s.NodeVersion)
fmt.Printf("Generated: %s\n\n", s.GeneratedAt)
// Group by category
byCategory := make(map[spec.Category][]spec.FlagSpec)
for _, f := range s.Flags {
byCategory[f.Category] = append(byCategory[f.Category], f)
}
// Sort categories
var cats []spec.Category
for cat := range byCategory {
cats = append(cats, cat)
}
sort.Slice(cats, func(i, j int) bool {
return string(cats[i]) < string(cats[j])
})
fmt.Println("## Table of Contents")
fmt.Println()
for _, cat := range cats {
fmt.Printf("- [%s](#%s)\n", s.Categories[cat], strings.ToLower(string(cat)))
}
fmt.Println()
for _, cat := range cats {
flags := byCategory[cat]
fmt.Printf("## %s\n\n", s.Categories[cat])
fmt.Printf("| Flag | Type | Default | Description |\n")
fmt.Printf("|------|------|---------|-------------|\n")
for _, f := range flags {
defaultStr := formatDefault(f.Default)
desc := strings.ReplaceAll(f.Description, "|", "\\|")
if f.Deprecated {
desc = "**DEPRECATED** " + desc
}
fmt.Printf("| `--%s` | %s | %s | %s |\n", f.Key, f.Type, defaultStr, desc)
}
fmt.Println()
}
}
func dumpGo(s *spec.ConfigSpec) {
fmt.Println("// Code generated by luxd-config spec --format=go. DO NOT EDIT.")
fmt.Println()
fmt.Printf("// Version: %s\n", s.Version)
fmt.Printf("// NodeVersion: %s\n", s.NodeVersion)
fmt.Println()
data, _ := json.Marshal(s)
fmt.Printf("const specJSON = `%s`\n", string(data))
}
func formatDefault(v interface{}) string {
if v == nil {
return "-"
}
switch val := v.(type) {
case string:
if val == "" {
return `""`
}
if len(val) > 30 {
return fmt.Sprintf(`"%s..."`, val[:27])
}
return fmt.Sprintf(`"%s"`, val)
case bool:
return fmt.Sprintf("%t", val)
case int, int64, uint, uint64:
return fmt.Sprintf("%d", val)
case float64:
return fmt.Sprintf("%.2f", val)
default:
return fmt.Sprintf("%v", val)
}
}
func validate(args []string) {
fs := pflag.NewFlagSet("validate", pflag.ExitOnError)
strict := fs.Bool("strict", false, "Fail on unknown keys")
quiet := fs.Bool("quiet", false, "Only output errors")
if err := fs.Parse(args); err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
if fs.NArg() < 1 {
fmt.Println("Error: config file path required")
fmt.Println("Usage: luxd-config validate <config-file>")
os.Exit(1)
}
configFile := fs.Arg(0)
// Load config file
v := viper.New()
v.SetConfigFile(configFile)
if err := v.ReadInConfig(); err != nil {
fmt.Printf("Error reading config: %v\n", err)
os.Exit(1)
}
s := spec.Spec()
knownKeys := make(map[string]bool)
for _, f := range s.Flags {
knownKeys[f.Key] = true
}
allKeys := v.AllKeys()
var unknownKeys []string
var deprecatedKeys []string
hasError := false
for _, key := range allKeys {
if !knownKeys[key] {
unknownKeys = append(unknownKeys, key)
if *strict {
hasError = true
}
} else {
// Check if deprecated
if f := s.GetFlag(key); f != nil && f.Deprecated {
deprecatedKeys = append(deprecatedKeys, key)
}
}
}
if !*quiet {
fmt.Printf("Validating: %s\n", configFile)
fmt.Printf("Keys found: %d\n", len(allKeys))
}
if len(unknownKeys) > 0 {
level := "Warning"
if *strict {
level = "Error"
}
fmt.Printf("\n%s: %d unknown key(s):\n", level, len(unknownKeys))
for _, key := range unknownKeys {
suggestion := findSimilar(key, s.Flags)
if suggestion != "" {
fmt.Printf(" - %s (did you mean: %s?)\n", key, suggestion)
} else {
fmt.Printf(" - %s\n", key)
}
}
}
if len(deprecatedKeys) > 0 {
fmt.Printf("\nWarning: %d deprecated key(s):\n", len(deprecatedKeys))
for _, key := range deprecatedKeys {
if f := s.GetFlag(key); f != nil {
msg := f.DeprecatedMessage
if msg == "" {
msg = "This flag is deprecated"
}
fmt.Printf(" - %s: %s\n", key, msg)
}
}
}
if hasError {
os.Exit(1)
}
if !*quiet {
fmt.Println("\nValidation passed!")
}
}
func showDiff(args []string) {
fs := pflag.NewFlagSet("diff", pflag.ExitOnError)
if err := fs.Parse(args); err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
if fs.NArg() < 1 {
fmt.Println("Error: config file path required")
fmt.Println("Usage: luxd-config diff <config-file>")
os.Exit(1)
}
configFile := fs.Arg(0)
v := viper.New()
v.SetConfigFile(configFile)
if err := v.ReadInConfig(); err != nil {
fmt.Printf("Error reading config: %v\n", err)
os.Exit(1)
}
s := spec.Spec()
fmt.Printf("Configuration differences from defaults:\n\n")
for _, f := range s.Flags {
if v.IsSet(f.Key) {
configVal := v.Get(f.Key)
defaultVal := f.Default
// Check if different from default
if !equalValues(configVal, defaultVal) {
fmt.Printf("%-45s default: %-20v config: %v\n",
f.Key, formatDefault(defaultVal), configVal)
}
}
}
}
func findSimilar(key string, flags []spec.FlagSpec) string {
// Simple Levenshtein-like matching
bestMatch := ""
bestScore := 0
for _, f := range flags {
score := similarity(key, f.Key)
if score > bestScore && score > len(key)/2 {
bestScore = score
bestMatch = f.Key
}
}
return bestMatch
}
func similarity(a, b string) int {
// Count common characters in sequence
score := 0
for i := 0; i < len(a) && i < len(b); i++ {
if a[i] == b[i] {
score++
}
}
// Bonus for shared substrings
for i := 0; i < len(a)-2; i++ {
substr := a[i : i+3]
if strings.Contains(b, substr) {
score += 2
}
}
return score
}
func equalValues(a, b interface{}) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
}