feat: Add dual CPU+GPU mining support with separate pools/algos

- 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 <noreply@anthropic.com>
This commit is contained in:
snider 2025-12-30 17:23:59 +00:00
parent ab47bae0a3
commit 876253f194
4 changed files with 313 additions and 15 deletions

View file

@ -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.

View file

@ -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)

View file

@ -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))
}

View file

@ -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,
}