Mining/pkg/mining/simulated_miner.go
Virgil 68c826a3d8
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
Align mining AX naming and comments
2026-04-04 05:33:08 +00:00

459 lines
13 KiB
Go

package mining
import (
"context"
"math"
"math/rand"
"strconv"
"sync"
"time"
)
// factory.Register(MinerTypeSimulated, func() Miner { return NewSimulatedMiner(SimulatedMinerConfig{}) })
const MinerTypeSimulated = "simulated"
// miner := &SimulatedMiner{Name: "sim-001", MinerType: MinerTypeSimulated, Algorithm: "rx/0"}
type SimulatedMiner struct {
// Exported fields for JSON serialization
Name string `json:"name"`
MinerType string `json:"miner_type"`
Version string `json:"version"`
URL string `json:"url"`
Path string `json:"path"`
MinerBinary string `json:"miner_binary"`
Running bool `json:"running"`
Algorithm string `json:"algorithm"`
HashrateHistory []HashratePoint `json:"hashrateHistory"`
LowResHistory []HashratePoint `json:"lowResHashrateHistory"`
Stats *PerformanceMetrics `json:"stats,omitempty"`
FullStats *XMRigSummary `json:"full_stats,omitempty"` // XMRig-compatible format for UI
// Internal fields (not exported)
baseHashrate int
peakHashrate int
variance float64
startTime time.Time
shares int
rejected int
logs []string
mutex sync.RWMutex
stopChan chan struct{}
poolName string
difficultyBase int
}
// SimulatedMinerConfig{Name: "sim-001", Algorithm: "rx/0", BaseHashrate: 5000, Variance: 0.1}
type SimulatedMinerConfig struct {
Name string // Miner instance name such as `sim-xmrig-001`.
Algorithm string // Algorithm name such as `rx/0`, `kawpow`, or `ethash`.
BaseHashrate int // Base hashrate in H/s
Variance float64 // Variance as percentage (0.0-0.2 for 20% variance)
PoolName string // Simulated pool name
Difficulty int // Base difficulty
}
// miner := NewSimulatedMiner(SimulatedMinerConfig{Name: "sim-001", Algorithm: "rx/0", BaseHashrate: 5000})
// miner.Start(ctx)
func NewSimulatedMiner(config SimulatedMinerConfig) *SimulatedMiner {
if config.Variance <= 0 {
config.Variance = 0.1 // Default 10% variance
}
if config.PoolName == "" {
config.PoolName = "sim-pool.example.com:3333"
}
if config.Difficulty <= 0 {
config.Difficulty = 10000
}
return &SimulatedMiner{
Name: config.Name,
MinerType: MinerTypeSimulated,
Version: "1.0.0-simulated",
URL: "https://github.com/simulated/miner",
Path: "/simulated/miner",
MinerBinary: "/simulated/miner/sim-miner",
Algorithm: config.Algorithm,
HashrateHistory: make([]HashratePoint, 0),
LowResHistory: make([]HashratePoint, 0),
baseHashrate: config.BaseHashrate,
variance: config.Variance,
poolName: config.PoolName,
difficultyBase: config.Difficulty,
logs: make([]string, 0),
}
}
// miner.GetType() == "simulated"
func (m *SimulatedMiner) GetType() string {
return m.MinerType
}
// if err := miner.Install(); err != nil { /* never errors */ }
func (m *SimulatedMiner) Install() error {
return nil
}
// if err := miner.Uninstall(); err != nil { /* never errors */ }
func (m *SimulatedMiner) Uninstall() error {
return nil
}
// if err := miner.Start(config); err != nil { /* already running */ }
func (m *SimulatedMiner) Start(config *Config) error {
m.mutex.Lock()
if m.Running {
m.mutex.Unlock()
return ErrMinerExists(m.Name)
}
m.Running = true
m.startTime = time.Now()
m.shares = 0
m.rejected = 0
m.stopChan = make(chan struct{})
m.HashrateHistory = make([]HashratePoint, 0)
m.LowResHistory = make([]HashratePoint, 0)
timestamp := time.Now().Format("15:04:05")
m.logs = []string{
"[" + timestamp + "] Simulated miner starting...",
"[" + timestamp + "] Connecting to " + m.poolName,
"[" + timestamp + "] Pool connected, algorithm: " + m.Algorithm,
}
m.mutex.Unlock()
// Start background simulation
go m.runSimulation()
return nil
}
// if err := miner.Stop(); err != nil { /* miner was not running */ }
func (m *SimulatedMiner) Stop() error {
m.mutex.Lock()
defer m.mutex.Unlock()
if !m.Running {
return ErrMinerNotRunning(m.Name)
}
close(m.stopChan)
m.Running = false
m.logs = append(m.logs, "["+time.Now().Format("15:04:05")+"] Miner stopped")
return nil
}
func (m *SimulatedMiner) runSimulation() {
ticker := time.NewTicker(HighResolutionInterval)
defer ticker.Stop()
shareTicker := time.NewTicker(time.Duration(5+rand.Intn(10)) * time.Second)
defer shareTicker.Stop()
for {
select {
case <-m.stopChan:
return
case <-ticker.C:
m.updateHashrate()
case <-shareTicker.C:
m.simulateShare()
// Randomize next share time
shareTicker.Reset(time.Duration(5+rand.Intn(15)) * time.Second)
}
}
}
func (m *SimulatedMiner) updateHashrate() {
m.mutex.Lock()
defer m.mutex.Unlock()
// Generate hashrate with variance and smooth transitions
now := time.Now()
uptime := now.Sub(m.startTime).Seconds()
// Ramp up period (first 30 seconds)
rampFactor := math.Min(1.0, uptime/30.0)
// Add some sine wave variation for realistic fluctuation
sineVariation := math.Sin(uptime/10) * 0.05
// Random noise
noise := (rand.Float64() - 0.5) * 2 * m.variance
// Calculate final hashrate
hashrate := int(float64(m.baseHashrate) * rampFactor * (1.0 + sineVariation + noise))
if hashrate < 0 {
hashrate = 0
}
point := HashratePoint{
Timestamp: now,
Hashrate: hashrate,
}
m.HashrateHistory = append(m.HashrateHistory, point)
// Track peak hashrate
if hashrate > m.peakHashrate {
m.peakHashrate = hashrate
}
// Update stats for JSON serialization
uptimeInt := int(uptime)
diffCurrent := m.difficultyBase + rand.Intn(m.difficultyBase/2)
m.Stats = &PerformanceMetrics{
Hashrate: hashrate,
Shares: m.shares,
Rejected: m.rejected,
Uptime: uptimeInt,
Algorithm: m.Algorithm,
AvgDifficulty: m.difficultyBase,
DiffCurrent: diffCurrent,
}
// Update XMRig-compatible full_stats for UI
m.FullStats = &XMRigSummary{
ID: m.Name,
WorkerID: m.Name,
Uptime: uptimeInt,
Algo: m.Algorithm,
Version: m.Version,
}
m.FullStats.Hashrate.Total = []float64{float64(hashrate)}
m.FullStats.Hashrate.Highest = float64(m.peakHashrate)
m.FullStats.Results.SharesGood = m.shares
m.FullStats.Results.SharesTotal = m.shares + m.rejected
m.FullStats.Results.DiffCurrent = diffCurrent
m.FullStats.Results.AvgTime = 15 + rand.Intn(10) // Simulated avg share time
m.FullStats.Results.HashesTotal = m.shares * diffCurrent
m.FullStats.Connection.Pool = m.poolName
m.FullStats.Connection.Uptime = uptimeInt
m.FullStats.Connection.Diff = diffCurrent
m.FullStats.Connection.Accepted = m.shares
m.FullStats.Connection.Rejected = m.rejected
m.FullStats.Connection.Algo = m.Algorithm
m.FullStats.Connection.Ping = 50 + rand.Intn(50)
// Trim high-res history to last 5 minutes
cutoff := now.Add(-HighResolutionDuration)
for len(m.HashrateHistory) > 0 && m.HashrateHistory[0].Timestamp.Before(cutoff) {
m.HashrateHistory = m.HashrateHistory[1:]
}
}
func (m *SimulatedMiner) simulateShare() {
m.mutex.Lock()
defer m.mutex.Unlock()
// 2% chance of rejected share
if rand.Float64() < 0.02 {
m.rejected++
m.logs = append(m.logs, "["+time.Now().Format("15:04:05")+"] Share rejected (stale)")
} else {
m.shares++
diff := m.difficultyBase + rand.Intn(m.difficultyBase/2)
m.logs = append(m.logs, "["+time.Now().Format("15:04:05")+"] Share accepted ("+strconv.Itoa(m.shares)+"/"+strconv.Itoa(m.rejected)+") diff "+strconv.Itoa(diff))
}
// Keep last 100 log lines
if len(m.logs) > 100 {
m.logs = m.logs[len(m.logs)-100:]
}
}
// metrics, err := miner.GetStats(ctx)
// _ = metrics.Hashrate // current H/s
// _ = metrics.Shares // accepted share count
func (m *SimulatedMiner) GetStats(ctx context.Context) (*PerformanceMetrics, error) {
m.mutex.RLock()
defer m.mutex.RUnlock()
if !m.Running {
return nil, ErrMinerNotRunning(m.Name)
}
// Calculate current hashrate from recent history
var hashrate int
if len(m.HashrateHistory) > 0 {
hashrate = m.HashrateHistory[len(m.HashrateHistory)-1].Hashrate
}
uptime := int(time.Since(m.startTime).Seconds())
// Calculate average difficulty
avgDiff := m.difficultyBase
if m.shares > 0 {
avgDiff = m.difficultyBase + rand.Intn(m.difficultyBase/4)
}
return &PerformanceMetrics{
Hashrate: hashrate,
Shares: m.shares,
Rejected: m.rejected,
Uptime: uptime,
LastShare: time.Now().Unix() - int64(rand.Intn(30)),
Algorithm: m.Algorithm,
AvgDifficulty: avgDiff,
DiffCurrent: m.difficultyBase + rand.Intn(m.difficultyBase/2),
ExtraData: map[string]interface{}{
"pool": m.poolName,
"simulated": true,
},
}, nil
}
// miner.GetName() == "sim-xmrig-001"
func (m *SimulatedMiner) GetName() string {
return m.Name
}
// miner.GetPath() == "/simulated/miner"
func (m *SimulatedMiner) GetPath() string {
return m.Path
}
// miner.GetBinaryPath() == "/simulated/miner/sim-miner"
func (m *SimulatedMiner) GetBinaryPath() string {
return m.MinerBinary
}
// details, _ := miner.CheckInstallation() // always reports IsInstalled: true
func (m *SimulatedMiner) CheckInstallation() (*InstallationDetails, error) {
return &InstallationDetails{
IsInstalled: true,
Version: "1.0.0-simulated",
Path: "/simulated/miner",
MinerBinary: "simulated-miner",
ConfigPath: "/simulated/config.json",
}, nil
}
// v, _ := miner.GetLatestVersion() // always "1.0.0-simulated", no network call
func (m *SimulatedMiner) GetLatestVersion() (string, error) {
return "1.0.0-simulated", nil
}
// points := miner.GetHashrateHistory() // snapshot of high-res window (last 5 min)
func (m *SimulatedMiner) GetHashrateHistory() []HashratePoint {
m.mutex.RLock()
defer m.mutex.RUnlock()
result := make([]HashratePoint, len(m.HashrateHistory))
copy(result, m.HashrateHistory)
return result
}
// miner.AddHashratePoint(HashratePoint{Timestamp: now, Hashrate: 5000})
func (m *SimulatedMiner) AddHashratePoint(point HashratePoint) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.HashrateHistory = append(m.HashrateHistory, point)
}
// manager.ReduceHashrateHistory(miner, time.Now())
func (m *SimulatedMiner) ReduceHashrateHistory(now time.Time) {
m.mutex.Lock()
defer m.mutex.Unlock()
// Move old high-res points to low-res
cutoff := now.Add(-HighResolutionDuration)
var toMove []HashratePoint
newHistory := make([]HashratePoint, 0)
for _, point := range m.HashrateHistory {
if point.Timestamp.Before(cutoff) {
toMove = append(toMove, point)
} else {
newHistory = append(newHistory, point)
}
}
m.HashrateHistory = newHistory
// Average the old points and add to low-res
if len(toMove) > 0 {
var sum int
for _, point := range toMove {
sum += point.Hashrate
}
avg := sum / len(toMove)
m.LowResHistory = append(m.LowResHistory, HashratePoint{
Timestamp: toMove[len(toMove)-1].Timestamp,
Hashrate: avg,
})
}
// Trim low-res history
lowResCutoff := now.Add(-LowResHistoryRetention)
newLowRes := make([]HashratePoint, 0)
for _, point := range m.LowResHistory {
if !point.Timestamp.Before(lowResCutoff) {
newLowRes = append(newLowRes, point)
}
}
m.LowResHistory = newLowRes
}
// logs := miner.GetLogs() // capped at 100 lines, includes share accept/reject events
func (m *SimulatedMiner) GetLogs() []string {
m.mutex.RLock()
defer m.mutex.RUnlock()
result := make([]string, len(m.logs))
copy(result, m.logs)
return result
}
// if err := miner.WriteStdin("h"); err != nil { /* not running */ }
func (m *SimulatedMiner) WriteStdin(input string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
if !m.Running {
return ErrMinerNotRunning(m.Name)
}
m.logs = append(m.logs, "["+time.Now().Format("15:04:05")+"] stdin: "+input)
return nil
}
// miner := NewSimulatedMiner(SimulatedMinerPresets["cpu-medium"])
// miner := NewSimulatedMiner(SimulatedMinerPresets["gpu-ethash"])
var SimulatedMinerPresets = map[string]SimulatedMinerConfig{
"cpu-low": {
Algorithm: "rx/0",
BaseHashrate: 500,
Variance: 0.15,
PoolName: "pool.hashvault.pro:443",
Difficulty: 50000,
},
"cpu-medium": {
Algorithm: "rx/0",
BaseHashrate: 5000,
Variance: 0.10,
PoolName: "pool.hashvault.pro:443",
Difficulty: 100000,
},
"cpu-high": {
Algorithm: "rx/0",
BaseHashrate: 15000,
Variance: 0.08,
PoolName: "pool.hashvault.pro:443",
Difficulty: 200000,
},
"gpu-ethash": {
Algorithm: "ethash",
BaseHashrate: 30000000, // 30 MH/s
Variance: 0.05,
PoolName: "eth.2miners.com:2020",
Difficulty: 4000000000,
},
"gpu-kawpow": {
Algorithm: "kawpow",
BaseHashrate: 15000000, // 15 MH/s
Variance: 0.06,
PoolName: "rvn.2miners.com:6060",
Difficulty: 1000000000,
},
}