package fleet import ( "context" "fmt" "time" ) // EarnBeforeBurnConfig gates sibling autospread until local mining proves viable. type EarnBeforeBurnConfig struct { MinHashrate float64 `json:"min_hashrate"` MinDuration time.Duration `json:"min_duration"` FirstTierOnly bool `json:"first_tier_only"` } // DefaultEarnConfig returns conservative earn-before-burn defaults. func DefaultEarnConfig() EarnBeforeBurnConfig { return EarnBeforeBurnConfig{ MinHashrate: 100.0, MinDuration: 5 * time.Minute, FirstTierOnly: true, } } // EarnGate tracks local hashrate proof before sibling spread. type EarnGate struct { Store *Store Config EarnBeforeBurnConfig } // SpreadDecision indicates whether sibling spread is allowed. type SpreadDecision struct { Allowed bool `json:"allowed"` Reason string `json:"reason"` LocalHashrate float64 `json:"local_hashrate"` Siblings []string `json:"siblings,omitempty"` } // CanSpreadToSiblings checks hashrate gate before autospread to phenotype siblings. func (g *EarnGate) CanSpreadToSiblings(ctx context.Context, hostID string) (*SpreadDecision, error) { dec := &SpreadDecision{} hr, err := g.Store.HostHashrate(ctx, hostID) if err != nil { dec.Reason = "host not found" return dec, err } dec.LocalHashrate = hr if hr < g.Config.MinHashrate { dec.Reason = fmt.Sprintf("hashrate %.2f below threshold %.2f", hr, g.Config.MinHashrate) return dec, nil } if g.Config.FirstTierOnly { attempts, err := g.Store.ListLOTL(ctx, hostID, 50) if err != nil { return dec, err } hasSuccess := false for _, a := range attempts { if a.Phase == "deploy" && a.Status == "success" { hasSuccess = true break } } if !hasSuccess { dec.Reason = "first tier deploy has not succeeded yet" return dec, nil } } var phenotype string err = g.Store.DB().QueryRowContext(ctx, `SELECT COALESCE(phenotype,'') FROM hosts WHERE id = ?`, hostID).Scan(&phenotype) if err != nil { dec.Reason = "phenotype unknown" return dec, err } siblings, err := g.Store.SiblingHosts(ctx, hostID, phenotype) if err != nil { return dec, err } dec.Allowed = len(siblings) > 0 dec.Siblings = siblings if !dec.Allowed { dec.Reason = "no siblings in phenotype group" } else { dec.Reason = "earn-before-burn gate passed" } return dec, nil }