From 876253f194c57e252088d6b5a57a7e65d9bb8061 Mon Sep 17 00:00:00 2001 From: snider Date: Tue, 30 Dec 2025 17:23:59 +0000 Subject: [PATCH] feat: Add dual CPU+GPU mining support with separate pools/algos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GPU config fields: GPUEnabled, GPUPool, GPUWallet, GPUAlgo, CUDA, OpenCL - XMRig config now supports separate pool/algo for GPU vs CPU mining - CPU can mine RandomX while GPU mines KawPow on different pools - Add xmrig_gpu_test.go with tests for dual, GPU-only, and CPU-only configs - Make getXMRigConfigPath a variable for test overriding 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- pkg/mining/mining.go | 17 ++- pkg/mining/xmrig.go | 3 +- pkg/mining/xmrig_gpu_test.go | 237 +++++++++++++++++++++++++++++++++++ pkg/mining/xmrig_start.go | 71 +++++++++-- 4 files changed, 313 insertions(+), 15 deletions(-) create mode 100644 pkg/mining/xmrig_gpu_test.go diff --git a/pkg/mining/mining.go b/pkg/mining/mining.go index 4885bb9..92edc04 100644 --- a/pkg/mining/mining.go +++ b/pkg/mining/mining.go @@ -115,10 +115,19 @@ type Config struct { Seed string `json:"seed,omitempty"` Hash string `json:"hash,omitempty"` NoDMI bool `json:"noDMI,omitempty"` - // GPU-specific options - Devices string `json:"devices,omitempty"` // GPU device selection (e.g., "0,1,2") - Intensity int `json:"intensity,omitempty"` // Mining intensity for GPU miners - CLIArgs string `json:"cliArgs,omitempty"` // Additional CLI arguments + // GPU-specific options (for XMRig dual CPU+GPU mining) + GPUEnabled bool `json:"gpuEnabled,omitempty"` // Enable GPU mining + GPUPool string `json:"gpuPool,omitempty"` // Separate pool for GPU (can differ from CPU) + GPUWallet string `json:"gpuWallet,omitempty"` // Wallet for GPU pool (defaults to main Wallet) + GPUAlgo string `json:"gpuAlgo,omitempty"` // Algorithm for GPU (e.g., "kawpow", "ethash") + GPUPassword string `json:"gpuPassword,omitempty"` // Password for GPU pool + GPUIntensity int `json:"gpuIntensity,omitempty"` // GPU mining intensity (0-100) + GPUThreads int `json:"gpuThreads,omitempty"` // GPU threads per card + Devices string `json:"devices,omitempty"` // GPU device selection (e.g., "0,1,2") + OpenCL bool `json:"opencl,omitempty"` // Enable OpenCL (AMD/Intel GPUs) + CUDA bool `json:"cuda,omitempty"` // Enable CUDA (NVIDIA GPUs) + Intensity int `json:"intensity,omitempty"` // Mining intensity for GPU miners + CLIArgs string `json:"cliArgs,omitempty"` // Additional CLI arguments } // PerformanceMetrics represents the performance metrics for a miner. diff --git a/pkg/mining/xmrig.go b/pkg/mining/xmrig.go index 0e18b9f..eb8fbea 100644 --- a/pkg/mining/xmrig.go +++ b/pkg/mining/xmrig.go @@ -48,7 +48,8 @@ func NewXMRigMiner() *XMRigMiner { // getXMRigConfigPath returns the platform-specific path for the xmrig.json file. // If instanceName is provided, it creates an instance-specific config file. -func getXMRigConfigPath(instanceName string) (string, error) { +// This is a variable so it can be overridden in tests. +var getXMRigConfigPath = func(instanceName string) (string, error) { configFileName := "xmrig.json" if instanceName != "" && instanceName != "xmrig" { // Use instance-specific config file (e.g., xmrig-78.json) diff --git a/pkg/mining/xmrig_gpu_test.go b/pkg/mining/xmrig_gpu_test.go new file mode 100644 index 0000000..3341ea0 --- /dev/null +++ b/pkg/mining/xmrig_gpu_test.go @@ -0,0 +1,237 @@ +package mining + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +func TestXMRigDualMiningConfig(t *testing.T) { + // Create a temp directory for the config + tmpDir := t.TempDir() + + miner := &XMRigMiner{ + BaseMiner: BaseMiner{ + Name: "xmrig-dual-test", + API: &API{ + Enabled: true, + ListenHost: "127.0.0.1", + ListenPort: 12345, + }, + }, + } + + // Temporarily override config path + origGetPath := getXMRigConfigPath + getXMRigConfigPath = func(name string) (string, error) { + return filepath.Join(tmpDir, name+".json"), nil + } + defer func() { getXMRigConfigPath = origGetPath }() + + // Config with CPU mining rx/0 and GPU mining kawpow on different pools + config := &Config{ + // CPU config + Pool: "stratum+tcp://pool.supportxmr.com:3333", + Wallet: "cpu_wallet_address", + Algo: "rx/0", + CPUMaxThreadsHint: 50, + + // GPU config - separate pool and algo + GPUEnabled: true, + GPUPool: "stratum+tcp://ravencoin.pool.com:3333", + GPUWallet: "gpu_wallet_address", + GPUAlgo: "kawpow", + CUDA: true, // NVIDIA + OpenCL: false, + } + + err := miner.createConfig(config) + if err != nil { + t.Fatalf("Failed to create config: %v", err) + } + + // Read and parse the generated config + data, err := os.ReadFile(miner.ConfigPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + + var generatedConfig map[string]interface{} + if err := json.Unmarshal(data, &generatedConfig); err != nil { + t.Fatalf("Failed to parse config: %v", err) + } + + // Verify pools + pools, ok := generatedConfig["pools"].([]interface{}) + if !ok { + t.Fatal("pools not found in config") + } + if len(pools) != 2 { + t.Errorf("Expected 2 pools (CPU + GPU), got %d", len(pools)) + } + + // Verify CPU pool + cpuPool := pools[0].(map[string]interface{}) + if cpuPool["url"] != "stratum+tcp://pool.supportxmr.com:3333" { + t.Errorf("CPU pool URL mismatch: %v", cpuPool["url"]) + } + if cpuPool["user"] != "cpu_wallet_address" { + t.Errorf("CPU wallet mismatch: %v", cpuPool["user"]) + } + if cpuPool["algo"] != "rx/0" { + t.Errorf("CPU algo mismatch: %v", cpuPool["algo"]) + } + + // Verify GPU pool + gpuPool := pools[1].(map[string]interface{}) + if gpuPool["url"] != "stratum+tcp://ravencoin.pool.com:3333" { + t.Errorf("GPU pool URL mismatch: %v", gpuPool["url"]) + } + if gpuPool["user"] != "gpu_wallet_address" { + t.Errorf("GPU wallet mismatch: %v", gpuPool["user"]) + } + if gpuPool["algo"] != "kawpow" { + t.Errorf("GPU algo mismatch: %v", gpuPool["algo"]) + } + + // Verify CUDA enabled, OpenCL disabled + cuda := generatedConfig["cuda"].(map[string]interface{}) + if cuda["enabled"] != true { + t.Error("CUDA should be enabled") + } + + opencl := generatedConfig["opencl"].(map[string]interface{}) + if opencl["enabled"] != false { + t.Error("OpenCL should be disabled") + } + + // Verify CPU config + cpu := generatedConfig["cpu"].(map[string]interface{}) + if cpu["enabled"] != true { + t.Error("CPU should be enabled") + } + if cpu["max-threads-hint"] != float64(50) { + t.Errorf("CPU max-threads-hint mismatch: %v", cpu["max-threads-hint"]) + } + + t.Logf("Generated dual-mining config:\n%s", string(data)) +} + +func TestXMRigGPUOnlyConfig(t *testing.T) { + tmpDir := t.TempDir() + + miner := &XMRigMiner{ + BaseMiner: BaseMiner{ + Name: "xmrig-gpu-only", + API: &API{ + Enabled: true, + ListenHost: "127.0.0.1", + ListenPort: 12346, + }, + }, + } + + origGetPath := getXMRigConfigPath + getXMRigConfigPath = func(name string) (string, error) { + return filepath.Join(tmpDir, name+".json"), nil + } + defer func() { getXMRigConfigPath = origGetPath }() + + // GPU-only config using same pool for simplicity + config := &Config{ + Pool: "stratum+tcp://pool.supportxmr.com:3333", + Wallet: "test_wallet", + Algo: "rx/0", + NoCPU: true, // Disable CPU + GPUEnabled: true, + OpenCL: true, // AMD GPU + CUDA: true, // Also NVIDIA + } + + err := miner.createConfig(config) + if err != nil { + t.Fatalf("Failed to create config: %v", err) + } + + data, err := os.ReadFile(miner.ConfigPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + + var generatedConfig map[string]interface{} + json.Unmarshal(data, &generatedConfig) + + // Both GPU backends should be enabled + cuda := generatedConfig["cuda"].(map[string]interface{}) + opencl := generatedConfig["opencl"].(map[string]interface{}) + + if cuda["enabled"] != true { + t.Error("CUDA should be enabled") + } + if opencl["enabled"] != true { + t.Error("OpenCL should be enabled") + } + + t.Logf("Generated GPU config:\n%s", string(data)) +} + +func TestXMRigCPUOnlyConfig(t *testing.T) { + tmpDir := t.TempDir() + + miner := &XMRigMiner{ + BaseMiner: BaseMiner{ + Name: "xmrig-cpu-only", + API: &API{ + Enabled: true, + ListenHost: "127.0.0.1", + ListenPort: 12347, + }, + }, + } + + origGetPath := getXMRigConfigPath + getXMRigConfigPath = func(name string) (string, error) { + return filepath.Join(tmpDir, name+".json"), nil + } + defer func() { getXMRigConfigPath = origGetPath }() + + // CPU-only config (GPUEnabled defaults to false) + config := &Config{ + Pool: "stratum+tcp://pool.supportxmr.com:3333", + Wallet: "test_wallet", + Algo: "rx/0", + } + + err := miner.createConfig(config) + if err != nil { + t.Fatalf("Failed to create config: %v", err) + } + + data, err := os.ReadFile(miner.ConfigPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + + var generatedConfig map[string]interface{} + json.Unmarshal(data, &generatedConfig) + + // GPU backends should be disabled + cuda := generatedConfig["cuda"].(map[string]interface{}) + opencl := generatedConfig["opencl"].(map[string]interface{}) + + if cuda["enabled"] != false { + t.Error("CUDA should be disabled for CPU-only config") + } + if opencl["enabled"] != false { + t.Error("OpenCL should be disabled for CPU-only config") + } + + // Should only have 1 pool + pools := generatedConfig["pools"].([]interface{}) + if len(pools) != 1 { + t.Errorf("Expected 1 pool for CPU-only, got %d", len(pools)) + } + + t.Logf("Generated CPU-only config:\n%s", string(data)) +} diff --git a/pkg/mining/xmrig_start.go b/pkg/mining/xmrig_start.go index a29818b..3380ef1 100644 --- a/pkg/mining/xmrig_start.go +++ b/pkg/mining/xmrig_start.go @@ -166,22 +166,73 @@ func (m *XMRigMiner) createConfig(config *Config) error { cpuConfig["priority"] = config.CPUPriority } + // Build pools array - CPU pool first + pools := []map[string]interface{}{ + { + "url": config.Pool, + "user": config.Wallet, + "pass": "x", + "keepalive": true, + "tls": config.TLS, + "algo": config.Algo, + }, + } + + // Add separate GPU pool if configured + if config.GPUEnabled && config.GPUPool != "" { + gpuWallet := config.GPUWallet + if gpuWallet == "" { + gpuWallet = config.Wallet // Default to main wallet + } + gpuPass := config.GPUPassword + if gpuPass == "" { + gpuPass = "x" + } + pools = append(pools, map[string]interface{}{ + "url": config.GPUPool, + "user": gpuWallet, + "pass": gpuPass, + "keepalive": true, + "algo": config.GPUAlgo, + }) + } + + // Build OpenCL (AMD/Intel GPU) config + openclConfig := map[string]interface{}{ + "enabled": config.GPUEnabled && config.OpenCL, + } + if config.GPUEnabled && config.OpenCL { + if config.GPUIntensity > 0 { + openclConfig["intensity"] = config.GPUIntensity + } + if config.GPUThreads > 0 { + openclConfig["threads"] = config.GPUThreads + } + } + + // Build CUDA (NVIDIA GPU) config + cudaConfig := map[string]interface{}{ + "enabled": config.GPUEnabled && config.CUDA, + } + if config.GPUEnabled && config.CUDA { + if config.GPUIntensity > 0 { + cudaConfig["intensity"] = config.GPUIntensity + } + if config.GPUThreads > 0 { + cudaConfig["threads"] = config.GPUThreads + } + } + c := map[string]interface{}{ "api": map[string]interface{}{ "enabled": m.API != nil && m.API.Enabled, "listen": apiListen, "restricted": true, }, - "pools": []map[string]interface{}{ - { - "url": config.Pool, - "user": config.Wallet, - "pass": "x", - "keepalive": true, - "tls": config.TLS, - }, - }, - "cpu": cpuConfig, + "pools": pools, + "cpu": cpuConfig, + "opencl": openclConfig, + "cuda": cudaConfig, "pause-on-active": config.PauseOnActive, "pause-on-battery": config.PauseOnBattery, }