Files
AetherForge/agent/miner/pool.go
drjones f7d6dcf542 Upgrade dashboard, builder, and agent resource controls.
Dark UI with hashrate graphs, inline setting help, percent-based threads/RAM, configurable process name and display modes, and LAN-aware run.bat startup banner.
2026-05-26 23:26:39 -07:00

201 lines
3.8 KiB
Go

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
engine *Engine
handler ShareHandler
mu sync.RWMutex
currentJob *job.Job
stopCh chan struct{}
wg sync.WaitGroup
paused atomic.Bool
hashesTotal atomic.Uint64
sharesFound atomic.Uint64
}
func NewPool(threads int, cfg config.RuntimeConfig, reporter *stats.Reporter, handler ShareHandler) *Pool {
if threads <= 0 {
threads = 1
}
return &Pool{
threads: threads,
cfg: cfg,
reporter: reporter,
engine: NewEngine(),
handler: handler,
stopCh: make(chan struct{}),
}
}
func (p *Pool) SetJob(job *job.Job) {
p.mu.Lock()
defer p.mu.Unlock()
p.currentJob = job
if job == nil {
return
}
seed := job.SeedHash
if seed == "" && len(job.Blob) >= 64 {
seed = job.Blob[:64]
}
if err := p.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)
}
go p.resourceGuard()
}
func (p *Pool) Stop() {
close(p.stopCh)
p.wg.Wait()
}
func (p *Pool) HashesPerSecond() float64 {
return float64(p.hashesTotal.Load())
}
func (p *Pool) ResetHashCounter() {
p.hashesTotal.Store(0)
}
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.resourcesOK())
}
}
}
func (p *Pool) resourcesOK() bool {
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) {
defer p.wg.Done()
var nonce uint32 = uint32(id * 1000000)
for {
select {
case <-p.stopCh:
return
default:
}
if p.paused.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
}
for batch := 0; batch < 256; batch++ {
select {
case <-p.stopCh:
return
default:
}
if p.paused.Load() {
break
}
hashHex, _, err := p.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)
if p.handler != nil {
p.handler(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)
}