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)
337 lines
8.1 KiB
Go
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
|
|
}
|