Complete private Monero miner control stack.
Implement Windows agent with RandomX mining and WebSocket fleet reporting, wire dashboard settings into the builder with saved exe paths, and add project README.
This commit is contained in:
65
agent/miner/engine.go
Normal file
65
agent/miner/engine.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package miner
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"sync"
|
||||
|
||||
"git.gammaspectra.live/P2Pool/go-randomx"
|
||||
)
|
||||
|
||||
const nonceOffset = 39
|
||||
const nonceSize = 4
|
||||
|
||||
type Engine struct {
|
||||
mu sync.RWMutex
|
||||
cache *randomx.Randomx_Cache
|
||||
vm *randomx.VM
|
||||
seedHex string
|
||||
blob []byte
|
||||
}
|
||||
|
||||
func NewEngine() *Engine {
|
||||
cache := randomx.Randomx_alloc_cache(0)
|
||||
return &Engine{cache: cache}
|
||||
}
|
||||
|
||||
func (e *Engine) SetJob(seedHex, blobHex string) error {
|
||||
seed, err := hex.DecodeString(seedHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blob, err := hex.DecodeString(blobHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.seedHex != seedHex {
|
||||
e.cache.Randomx_init_cache(seed)
|
||||
e.vm = e.cache.VM_Initialize()
|
||||
e.seedHex = seedHex
|
||||
}
|
||||
e.blob = append([]byte(nil), blob...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) HashAtNonce(nonce uint32) (hashHex string, blobHex string, err error) {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
|
||||
if e.vm == nil || len(e.blob) < nonceOffset+nonceSize {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
work := append([]byte(nil), e.blob...)
|
||||
work[nonceOffset] = byte(nonce)
|
||||
work[nonceOffset+1] = byte(nonce >> 8)
|
||||
work[nonceOffset+2] = byte(nonce >> 16)
|
||||
work[nonceOffset+3] = byte(nonce >> 24)
|
||||
|
||||
out := make([]byte, 32)
|
||||
e.vm.CalculateHash(work, out)
|
||||
return hex.EncodeToString(out), hex.EncodeToString(work), nil
|
||||
}
|
||||
146
agent/miner/pool.go
Normal file
146
agent/miner/pool.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package miner
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"math/big"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/job"
|
||||
)
|
||||
|
||||
type ShareHandler func(jobID, nonce, hash string)
|
||||
|
||||
type Pool struct {
|
||||
threads int
|
||||
engine *Engine
|
||||
handler ShareHandler
|
||||
|
||||
mu sync.RWMutex
|
||||
currentJob *job.Job
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
|
||||
hashesTotal atomic.Uint64
|
||||
sharesFound atomic.Uint64
|
||||
}
|
||||
|
||||
func NewPool(threads int, handler ShareHandler) *Pool {
|
||||
if threads <= 0 {
|
||||
threads = 1
|
||||
}
|
||||
return &Pool{
|
||||
threads: threads,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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) worker(id int) {
|
||||
defer p.wg.Done()
|
||||
|
||||
var nonce uint32 = uint32(id * 1000000)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
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:
|
||||
}
|
||||
|
||||
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 hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
53
agent/miner/target.go
Normal file
53
agent/miner/target.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package miner
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func hashMeetsTarget(hashHex, targetHex string) bool {
|
||||
hashBytes, err := hex.DecodeString(hashHex)
|
||||
if err != nil || len(hashBytes) == 0 {
|
||||
return false
|
||||
}
|
||||
targetBytes, err := hex.DecodeString(padHex(targetHex, len(hashBytes)*2))
|
||||
if err != nil || len(targetBytes) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(targetBytes) < len(hashBytes) {
|
||||
padded := make([]byte, len(hashBytes))
|
||||
copy(padded, targetBytes)
|
||||
targetBytes = padded
|
||||
}
|
||||
if len(hashBytes) < len(targetBytes) {
|
||||
padded := make([]byte, len(targetBytes))
|
||||
copy(padded, hashBytes)
|
||||
hashBytes = padded
|
||||
}
|
||||
|
||||
hashInt := new(big.Int).SetBytes(reverseBytes(hashBytes))
|
||||
targetInt := new(big.Int).SetBytes(reverseBytes(targetBytes))
|
||||
return hashInt.Cmp(targetInt) <= 0
|
||||
}
|
||||
|
||||
func padHex(s string, length int) string {
|
||||
if len(s) >= length {
|
||||
return s
|
||||
}
|
||||
pad := length - len(s)
|
||||
out := make([]byte, length)
|
||||
for i := 0; i < pad; i++ {
|
||||
out[i] = '0'
|
||||
}
|
||||
copy(out[pad:], []byte(s))
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func reverseBytes(b []byte) []byte {
|
||||
out := make([]byte, len(b))
|
||||
for i := range b {
|
||||
out[i] = b[len(b)-1-i]
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user