mirror of
https://github.com/luxfi/vm.git
synced 2026-10-30 15:13:40 +00:00
339 lines
8.2 KiB
Go
339 lines
8.2 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package tmpnet
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"net/netip"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"syscall"
|
|
|
|
"github.com/luxfi/crypto/bls"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/log"
|
|
luxtls "github.com/luxfi/tls"
|
|
)
|
|
|
|
// 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.MarshalIndent(config, "", " ")
|
|
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 := luxtls.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
|
|
}
|