Files
node/tests/fixture/tmpnet/node.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

337 lines
8.1 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package tmpnet
import (
"context"
"crypto/rand"
"crypto/x509"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"encoding/pem"
"fmt"
"net/netip"
"os"
"os/exec"
"path/filepath"
"syscall"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/node/staking"
)
// Node represents a node in a local test network
type Node struct {
NodeID ids.NodeID
URI string
StakingPort uint64
HTTPPort uint64
StakingAddress netip.AddrPort
DataDir string
RuntimeConfig *NodeRuntimeConfig
Flags Flags
// BLS signing key
SigningKey *bls.SecretKey
// Staking credentials
StakingKey []byte
StakingCert []byte
// Process management
cmd *exec.Cmd
}
// NodeConfig is the serialized node configuration
type NodeConfig struct {
NodeID string `json:"nodeID"`
URI string `json:"uri"`
StakingPort uint64 `json:"stakingPort"`
HTTPPort uint64 `json:"httpPort"`
DataDir string `json:"dataDir"`
Flags map[string]interface{} `json:"flags"`
}
// NewNode creates a new node with default configuration
func NewNode() *Node {
return &Node{
Flags: make(Flags),
}
}
// ReadNode reads a node configuration from its directory
func ReadNode(nodeDir string) (*Node, error) {
configPath := filepath.Join(nodeDir, "node.json")
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read node config: %w", err)
}
var config NodeConfig
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse node config: %w", err)
}
nodeID, err := ids.NodeIDFromString(config.NodeID)
if err != nil {
return nil, fmt.Errorf("failed to parse node ID: %w", err)
}
node := &Node{
NodeID: nodeID,
URI: config.URI,
StakingPort: config.StakingPort,
HTTPPort: config.HTTPPort,
DataDir: config.DataDir,
Flags: config.Flags,
}
// Read staking credentials if present
stakingKeyPath := filepath.Join(nodeDir, "staking", "staker.key")
stakingCertPath := filepath.Join(nodeDir, "staking", "staker.crt")
if keyData, err := os.ReadFile(stakingKeyPath); err == nil {
node.StakingKey = keyData
}
if certData, err := os.ReadFile(stakingCertPath); err == nil {
node.StakingCert = certData
}
return node, nil
}
// Write writes the node configuration to disk
func (n *Node) Write() error {
if n.DataDir == "" {
return fmt.Errorf("node data directory not set")
}
if err := os.MkdirAll(n.DataDir, 0755); err != nil {
return fmt.Errorf("failed to create node directory: %w", err)
}
config := NodeConfig{
NodeID: n.NodeID.String(),
URI: n.URI,
StakingPort: n.StakingPort,
HTTPPort: n.HTTPPort,
DataDir: n.DataDir,
Flags: n.Flags,
}
data, err := json.Marshal(config, jsontext.WithIndent(" "))
if err != nil {
return fmt.Errorf("failed to marshal node config: %w", err)
}
configPath := filepath.Join(n.DataDir, "node.json")
if err := os.WriteFile(configPath, data, 0644); err != nil {
return fmt.Errorf("failed to write node config: %w", err)
}
// Write staking credentials if present
if len(n.StakingKey) > 0 || len(n.StakingCert) > 0 {
stakingDir := filepath.Join(n.DataDir, "staking")
if err := os.MkdirAll(stakingDir, 0700); err != nil {
return fmt.Errorf("failed to create staking directory: %w", err)
}
if len(n.StakingKey) > 0 {
keyPath := filepath.Join(stakingDir, "staker.key")
if err := os.WriteFile(keyPath, n.StakingKey, 0600); err != nil {
return fmt.Errorf("failed to write staking key: %w", err)
}
}
if len(n.StakingCert) > 0 {
certPath := filepath.Join(stakingDir, "staker.crt")
if err := os.WriteFile(certPath, n.StakingCert, 0644); err != nil {
return fmt.Errorf("failed to write staking cert: %w", err)
}
}
}
return nil
}
// Start starts the node process
func (n *Node) Start(ctx context.Context, log log.Logger) error {
if n.RuntimeConfig == nil || n.RuntimeConfig.Process == nil {
return fmt.Errorf("node runtime config not set")
}
luxdPath := n.RuntimeConfig.Process.LuxdPath
if luxdPath == "" {
return fmt.Errorf("luxd path not set")
}
args := []string{}
for k, v := range n.Flags {
args = append(args, fmt.Sprintf("--%s=%v", k, v))
}
n.cmd = exec.CommandContext(ctx, luxdPath, args...)
n.cmd.Dir = n.DataDir
if err := n.cmd.Start(); err != nil {
return fmt.Errorf("failed to start node: %w", err)
}
log.Info("started node", "nodeID", n.NodeID, "pid", n.cmd.Process.Pid)
return nil
}
// Stop stops the node process
func (n *Node) Stop(ctx context.Context) error {
if n.cmd == nil || n.cmd.Process == nil {
return nil
}
// Send SIGTERM first
if err := n.cmd.Process.Signal(syscall.SIGTERM); err != nil {
// Process may already be dead
return nil
}
// Wait for process to exit
done := make(chan error, 1)
go func() {
done <- n.cmd.Wait()
}()
select {
case <-ctx.Done():
// Force kill if context expires
n.cmd.Process.Kill()
return ctx.Err()
case err := <-done:
return err
}
}
// InitiateStop sends SIGTERM to the node process without waiting
func (n *Node) InitiateStop() error {
if n.cmd == nil || n.cmd.Process == nil {
return nil
}
return n.cmd.Process.Signal(syscall.SIGTERM)
}
// WaitForStopped waits for the node process to exit
func (n *Node) WaitForStopped(ctx context.Context) error {
if n.cmd == nil {
return nil
}
done := make(chan error, 1)
go func() {
done <- n.cmd.Wait()
}()
select {
case <-ctx.Done():
return ctx.Err()
case err := <-done:
return err
}
}
// IsRunning returns true if the node process is running
func (n *Node) IsRunning() bool {
if n.cmd == nil || n.cmd.Process == nil {
return false
}
// Check if process is still running
err := n.cmd.Process.Signal(syscall.Signal(0))
return err == nil
}
// GetURI returns the node's HTTP URI
func (n *Node) GetURI() string {
if n.URI == "" {
return fmt.Sprintf("http://127.0.0.1:%d", n.HTTPPort)
}
return fmt.Sprintf("http://%s:%d", n.URI, n.HTTPPort)
}
// EnsureKeys ensures the node has staking credentials and derives NodeID from them
func (n *Node) EnsureKeys() error {
if len(n.StakingKey) > 0 && len(n.StakingCert) > 0 && n.NodeID != ids.EmptyNodeID {
// Already have keys and node ID
return nil
}
// Generate new staking credentials
cert, key, err := staking.NewCertAndKeyBytes()
if err != nil {
return fmt.Errorf("failed to generate staking credentials: %w", err)
}
n.StakingKey = key
n.StakingCert = cert
// Derive NodeID from certificate
nodeID, err := deriveNodeID(cert)
if err != nil {
return fmt.Errorf("failed to derive node ID: %w", err)
}
n.NodeID = nodeID
// Generate BLS signing key if not present
if n.SigningKey == nil {
sk, err := bls.NewSecretKey()
if err != nil {
return fmt.Errorf("failed to generate BLS key: %w", err)
}
n.SigningKey = sk
}
return nil
}
// deriveNodeID derives a NodeID from a staking certificate
func deriveNodeID(certBytes []byte) (ids.NodeID, error) {
block, _ := pem.Decode(certBytes)
if block == nil {
return ids.EmptyNodeID, fmt.Errorf("failed to decode certificate PEM")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return ids.EmptyNodeID, fmt.Errorf("failed to parse certificate: %w", err)
}
// Convert x509.Certificate to ids.Certificate
idsCert := &ids.Certificate{
Raw: cert.Raw,
PublicKey: cert.PublicKey,
}
return ids.NodeIDFromCert(idsCert), nil
}
// EnsureBLSSigningKey ensures the node has a BLS signing key
func (n *Node) EnsureBLSSigningKey() error {
if n.SigningKey != nil {
return nil
}
sk, err := bls.NewSecretKey()
if err != nil {
return fmt.Errorf("failed to generate BLS signing key: %w", err)
}
n.SigningKey = sk
return nil
}
// GenerateNodeID generates a random NodeID for testing purposes
func GenerateNodeID() (ids.NodeID, error) {
var nodeID ids.NodeID
if _, err := rand.Read(nodeID[:]); err != nil {
return ids.EmptyNodeID, fmt.Errorf("failed to generate random node ID: %w", err)
}
return nodeID, nil
}