mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
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)
366 lines
7.8 KiB
Go
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)
|
|
}
|