Mining/cmd/mining/cmd/remote.go
snider 9a781ae3f0 feat: Add multi-node P2P mining management system
Implement secure peer-to-peer communication between Mining CLI instances
for remote control of mining rigs. Uses Borg library for encryption
(SMSG, STMF, TIM) and Poindexter for KD-tree based peer selection.

Features:
- Node identity management with X25519 keypairs
- Peer registry with multi-factor optimization (ping/hops/geo/score)
- WebSocket transport with SMSG encryption
- Controller/Worker architecture for remote operations
- TIM/STIM encrypted bundles for profile/miner deployment
- CLI commands: node, peer, remote
- REST API endpoints for node/peer/remote operations
- Docker support for P2P testing with multiple nodes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 19:49:33 +00:00

410 lines
10 KiB
Go

package cmd
import (
"fmt"
"strings"
"time"
"github.com/Snider/Mining/pkg/node"
"github.com/spf13/cobra"
)
var (
controller *node.Controller
transport *node.Transport
)
// remoteCmd represents the remote parent command
var remoteCmd = &cobra.Command{
Use: "remote",
Short: "Control remote mining nodes",
Long: `Send commands to remote worker nodes and retrieve their status.`,
}
// remoteStatusCmd shows stats from remote peers
var remoteStatusCmd = &cobra.Command{
Use: "status [peer-id]",
Short: "Get mining status from remote peers",
Long: `Display mining statistics from all connected peers or a specific peer.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctrl, err := getController()
if err != nil {
return err
}
if len(args) > 0 {
// Get stats from specific peer
peerID := args[0]
peer := findPeerByPartialID(peerID)
if peer == nil {
return fmt.Errorf("peer not found: %s", peerID)
}
stats, err := ctrl.GetRemoteStats(peer.ID)
if err != nil {
return fmt.Errorf("failed to get stats: %w", err)
}
printPeerStats(peer, stats)
} else {
// Get stats from all peers
allStats := ctrl.GetAllStats()
if len(allStats) == 0 {
fmt.Println("No connected peers.")
return nil
}
pr, _ := getPeerRegistry()
var totalHashrate float64
for peerID, stats := range allStats {
peer := pr.GetPeer(peerID)
if peer != nil {
printPeerStats(peer, stats)
for _, miner := range stats.Miners {
totalHashrate += miner.Hashrate
}
}
}
fmt.Println("────────────────────────────────────")
fmt.Printf("Total Fleet Hashrate: %.2f H/s\n", totalHashrate)
}
return nil
},
}
// remoteStartCmd starts a miner on a remote peer
var remoteStartCmd = &cobra.Command{
Use: "start <peer-id>",
Short: "Start miner on remote peer",
Long: `Start a miner on a remote peer using a profile.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
profileID, _ := cmd.Flags().GetString("profile")
if profileID == "" {
return fmt.Errorf("--profile is required")
}
peerID := args[0]
peer := findPeerByPartialID(peerID)
if peer == nil {
return fmt.Errorf("peer not found: %s", peerID)
}
ctrl, err := getController()
if err != nil {
return err
}
fmt.Printf("Starting miner on %s with profile %s...\n", peer.Name, profileID)
if err := ctrl.StartRemoteMiner(peer.ID, profileID, nil); err != nil {
return fmt.Errorf("failed to start miner: %w", err)
}
fmt.Println("Miner started successfully.")
return nil
},
}
// remoteStopCmd stops a miner on a remote peer
var remoteStopCmd = &cobra.Command{
Use: "stop <peer-id> [miner-name]",
Short: "Stop miner on remote peer",
Long: `Stop a running miner on a remote peer.`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
peerID := args[0]
peer := findPeerByPartialID(peerID)
if peer == nil {
return fmt.Errorf("peer not found: %s", peerID)
}
minerName := ""
if len(args) > 1 {
minerName = args[1]
} else {
minerName, _ = cmd.Flags().GetString("miner")
}
if minerName == "" {
return fmt.Errorf("miner name required (as argument or --miner flag)")
}
ctrl, err := getController()
if err != nil {
return err
}
fmt.Printf("Stopping miner %s on %s...\n", minerName, peer.Name)
if err := ctrl.StopRemoteMiner(peer.ID, minerName); err != nil {
return fmt.Errorf("failed to stop miner: %w", err)
}
fmt.Println("Miner stopped successfully.")
return nil
},
}
// remoteLogsCmd gets logs from a remote miner
var remoteLogsCmd = &cobra.Command{
Use: "logs <peer-id> <miner-name>",
Short: "Get console logs from remote miner",
Long: `Retrieve console output logs from a miner running on a remote peer.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
peerID := args[0]
minerName := args[1]
lines, _ := cmd.Flags().GetInt("lines")
peer := findPeerByPartialID(peerID)
if peer == nil {
return fmt.Errorf("peer not found: %s", peerID)
}
ctrl, err := getController()
if err != nil {
return err
}
logLines, err := ctrl.GetRemoteLogs(peer.ID, minerName, lines)
if err != nil {
return fmt.Errorf("failed to get logs: %w", err)
}
fmt.Printf("Logs from %s on %s (%d lines):\n", minerName, peer.Name, len(logLines))
fmt.Println("────────────────────────────────────")
for _, line := range logLines {
fmt.Println(line)
}
return nil
},
}
// remoteConnectCmd connects to a peer
var remoteConnectCmd = &cobra.Command{
Use: "connect <peer-id>",
Short: "Connect to a remote peer",
Long: `Establish a WebSocket connection to a registered peer.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
peerID := args[0]
peer := findPeerByPartialID(peerID)
if peer == nil {
return fmt.Errorf("peer not found: %s", peerID)
}
ctrl, err := getController()
if err != nil {
return err
}
fmt.Printf("Connecting to %s at %s...\n", peer.Name, peer.Address)
if err := ctrl.ConnectToPeer(peer.ID); err != nil {
return fmt.Errorf("failed to connect: %w", err)
}
fmt.Println("Connected successfully.")
return nil
},
}
// remoteDisconnectCmd disconnects from a peer
var remoteDisconnectCmd = &cobra.Command{
Use: "disconnect <peer-id>",
Short: "Disconnect from a remote peer",
Long: `Close the connection to a peer.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
peerID := args[0]
peer := findPeerByPartialID(peerID)
if peer == nil {
return fmt.Errorf("peer not found: %s", peerID)
}
ctrl, err := getController()
if err != nil {
return err
}
fmt.Printf("Disconnecting from %s...\n", peer.Name)
if err := ctrl.DisconnectFromPeer(peer.ID); err != nil {
return fmt.Errorf("failed to disconnect: %w", err)
}
fmt.Println("Disconnected.")
return nil
},
}
// remotePingCmd pings a peer
var remotePingCmd = &cobra.Command{
Use: "ping <peer-id>",
Short: "Ping a remote peer",
Long: `Send a ping to a peer and measure round-trip latency.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
count, _ := cmd.Flags().GetInt("count")
peerID := args[0]
peer := findPeerByPartialID(peerID)
if peer == nil {
return fmt.Errorf("peer not found: %s", peerID)
}
ctrl, err := getController()
if err != nil {
return err
}
fmt.Printf("Pinging %s (%s)...\n", peer.Name, peer.Address)
var totalRTT float64
var successful int
for i := 0; i < count; i++ {
rtt, err := ctrl.PingPeer(peer.ID)
if err != nil {
fmt.Printf(" Ping %d: timeout\n", i+1)
continue
}
fmt.Printf(" Ping %d: %.2f ms\n", i+1, rtt)
totalRTT += rtt
successful++
if i < count-1 {
time.Sleep(time.Second)
}
}
if successful > 0 {
fmt.Printf("\nAverage: %.2f ms (%d/%d successful)\n", totalRTT/float64(successful), successful, count)
} else {
fmt.Println("\nAll pings failed.")
}
return nil
},
}
func init() {
rootCmd.AddCommand(remoteCmd)
// remote status
remoteCmd.AddCommand(remoteStatusCmd)
// remote start
remoteCmd.AddCommand(remoteStartCmd)
remoteStartCmd.Flags().StringP("profile", "p", "", "Profile ID to use for starting the miner")
// remote stop
remoteCmd.AddCommand(remoteStopCmd)
remoteStopCmd.Flags().StringP("miner", "m", "", "Miner name to stop")
// remote logs
remoteCmd.AddCommand(remoteLogsCmd)
remoteLogsCmd.Flags().IntP("lines", "n", 100, "Number of log lines to retrieve")
// remote connect
remoteCmd.AddCommand(remoteConnectCmd)
// remote disconnect
remoteCmd.AddCommand(remoteDisconnectCmd)
// remote ping
remoteCmd.AddCommand(remotePingCmd)
remotePingCmd.Flags().IntP("count", "c", 4, "Number of pings to send")
}
// getController returns or creates the controller instance.
func getController() (*node.Controller, error) {
if controller != nil {
return controller, nil
}
nm, err := getNodeManager()
if err != nil {
return nil, fmt.Errorf("failed to get node manager: %w", err)
}
if !nm.HasIdentity() {
return nil, fmt.Errorf("no node identity found. Run 'node init' first")
}
pr, err := getPeerRegistry()
if err != nil {
return nil, fmt.Errorf("failed to get peer registry: %w", err)
}
// Initialize transport if not done
if transport == nil {
config := node.DefaultTransportConfig()
transport = node.NewTransport(nm, pr, config)
}
controller = node.NewController(nm, pr, transport)
return controller, nil
}
// findPeerByPartialID finds a peer by full or partial ID.
func findPeerByPartialID(partialID string) *node.Peer {
pr, err := getPeerRegistry()
if err != nil {
return nil
}
// Try exact match first
peer := pr.GetPeer(partialID)
if peer != nil {
return peer
}
// Try partial match
for _, p := range pr.ListPeers() {
if strings.HasPrefix(p.ID, partialID) {
return p
}
// Also try matching by name
if strings.EqualFold(p.Name, partialID) {
return p
}
}
return nil
}
// printPeerStats prints formatted stats for a peer.
func printPeerStats(peer *node.Peer, stats *node.StatsPayload) {
fmt.Printf("\n%s (%s)\n", peer.Name, peer.ID[:16])
fmt.Printf(" Address: %s\n", peer.Address)
fmt.Printf(" Uptime: %s\n", formatDuration(time.Duration(stats.Uptime)*time.Second))
fmt.Printf(" Miners: %d\n", len(stats.Miners))
if len(stats.Miners) > 0 {
fmt.Println()
for _, miner := range stats.Miners {
fmt.Printf(" %s (%s)\n", miner.Name, miner.Type)
fmt.Printf(" Hashrate: %.2f H/s\n", miner.Hashrate)
fmt.Printf(" Shares: %d (rejected: %d)\n", miner.Shares, miner.Rejected)
fmt.Printf(" Algorithm: %s\n", miner.Algorithm)
fmt.Printf(" Pool: %s\n", miner.Pool)
}
}
}
// formatDuration formats a duration into a human-readable string.
func formatDuration(d time.Duration) string {
days := int(d.Hours() / 24)
hours := int(d.Hours()) % 24
minutes := int(d.Minutes()) % 60
if days > 0 {
return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
}
if hours > 0 {
return fmt.Sprintf("%dh %dm", hours, minutes)
}
return fmt.Sprintf("%dm", minutes)
}