Add Fleet Torrent erasure extension with shard DHT and gossip.
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
Content-addressed shard DHT on seeder agents with subnet_primary_seeder election, cross-subnet fleet_torrent_gossip, BGP swarm magnets, C2 torrent manifest, and k-of-n peer fetch with C2 fallback.
This commit is contained in:
101
server/internal/erasure/torrent.go
Normal file
101
server/internal/erasure/torrent.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package erasure
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TorrentShardEntry is one shard in a published torrent manifest.
|
||||
type TorrentShardEntry struct {
|
||||
Index int `json:"index"`
|
||||
ShardHash string `json:"shard_hash"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// TorrentManifest is the C2 super-seeder torrent manifest for a deploy-plan token.
|
||||
type TorrentManifest struct {
|
||||
Token string `json:"token"`
|
||||
Scheme string `json:"scheme"`
|
||||
DataShards int `json:"data_shards"`
|
||||
ParityShards int `json:"parity_shards"`
|
||||
PayloadSHA256 string `json:"sha256"`
|
||||
PayloadSize int `json:"payload_size"`
|
||||
SwarmMagnet string `json:"swarm_magnet"`
|
||||
ManifestURL string `json:"manifest_url"`
|
||||
ShardManifestURLs []string `json:"shard_manifest_urls"`
|
||||
Shards []TorrentShardEntry `json:"shards"`
|
||||
}
|
||||
|
||||
// ShardContentHash returns the content-address hex SHA256 of one encoded shard.
|
||||
func ShardContentHash(shard []byte) string {
|
||||
sum := sha256.Sum256(shard)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// ShardContentHashes returns content-address hashes for each shard slice.
|
||||
func ShardContentHashes(shards [][]byte) []string {
|
||||
out := make([]string, len(shards))
|
||||
for i, sh := range shards {
|
||||
out[i] = ShardContentHash(sh)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// BuildTorrentManifest publishes canonical shard URLs and a swarm magnet for BGP hints.
|
||||
func BuildTorrentManifest(serverURL, token, payloadSHA string, payloadSize int, p Params, shardHashes []string) (*TorrentManifest, error) {
|
||||
p, err := p.Normalize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
total := p.TotalShards()
|
||||
if len(shardHashes) != total {
|
||||
return nil, fmt.Errorf("erasure torrent: shard hash count mismatch")
|
||||
}
|
||||
base := strings.TrimRight(strings.TrimSpace(serverURL), "/")
|
||||
if base == "" {
|
||||
base = "http://127.0.0.1:8989"
|
||||
}
|
||||
manifestURL := fmt.Sprintf("%s/api/v1/public/erasure-torrent/%s/manifest", base, token)
|
||||
shards := make([]TorrentShardEntry, total)
|
||||
urls := make([]string, total)
|
||||
for i := 0; i < total; i++ {
|
||||
shardURL := fmt.Sprintf("%s/api/v1/public/erasure-shard/%s/%d", base, token, i)
|
||||
shards[i] = TorrentShardEntry{Index: i, ShardHash: shardHashes[i], URL: shardURL}
|
||||
urls[i] = shardURL
|
||||
}
|
||||
return &TorrentManifest{
|
||||
Token: token,
|
||||
Scheme: SchemeReedSolomonV1,
|
||||
DataShards: p.DataShards,
|
||||
ParityShards: p.ParityShards,
|
||||
PayloadSHA256: strings.TrimSpace(strings.ToLower(payloadSHA)),
|
||||
PayloadSize: payloadSize,
|
||||
SwarmMagnet: SwarmMagnetLink(token, payloadSHA),
|
||||
ManifestURL: manifestURL,
|
||||
ShardManifestURLs: urls,
|
||||
Shards: shards,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SwarmMagnetLink builds a magnet URI for fleet torrent swarm discovery.
|
||||
func SwarmMagnetLink(token, payloadSHA string) string {
|
||||
token = strings.TrimSpace(token)
|
||||
payloadSHA = strings.TrimSpace(strings.ToLower(payloadSHA))
|
||||
if token == "" {
|
||||
return ""
|
||||
}
|
||||
q := url.Values{}
|
||||
if payloadSHA != "" {
|
||||
q.Set("xt", "urn:sha256:"+payloadSHA)
|
||||
}
|
||||
label := token
|
||||
if len(label) > 8 {
|
||||
label = label[:8]
|
||||
}
|
||||
q.Set("dn", "aetherforge-erasure-"+label)
|
||||
q.Set("tr", "urn:aetherforge:erasure:"+token)
|
||||
return "magnet:?" + q.Encode()
|
||||
}
|
||||
49
server/internal/erasure/torrent_test.go
Normal file
49
server/internal/erasure/torrent_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package erasure
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBuildTorrentManifestMagnet(t *testing.T) {
|
||||
p := DefaultParams()
|
||||
hashes := []string{"aa", "bb", "cc", "dd", "ee", "ff"}
|
||||
m, err := BuildTorrentManifest("http://c2:8989", "deadbeef", "abc123", 1024, p, hashes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m.SwarmMagnet == "" || m.ManifestURL == "" {
|
||||
t.Fatalf("manifest=%+v", m)
|
||||
}
|
||||
if len(m.ShardManifestURLs) != 6 {
|
||||
t.Fatalf("urls=%d", len(m.ShardManifestURLs))
|
||||
}
|
||||
if m.Shards[0].URL != "http://c2:8989/api/v1/public/erasure-shard/deadbeef/0" {
|
||||
t.Fatalf("shard url=%q", m.Shards[0].URL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwarmMagnetLink(t *testing.T) {
|
||||
m := SwarmMagnetLink("tok12345678", "deadbeef")
|
||||
if m == "" || !contains(m, "urn:aetherforge:erasure:tok12345678") {
|
||||
t.Fatalf("magnet=%q", m)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, sub string) bool {
|
||||
return len(s) >= len(sub) && (s == sub || len(sub) == 0 || indexOf(s, sub) >= 0)
|
||||
}
|
||||
|
||||
func indexOf(s, sub string) int {
|
||||
for i := 0; i+len(sub) <= len(s); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func TestShardContentHash(t *testing.T) {
|
||||
h1 := ShardContentHash([]byte("shard-a"))
|
||||
h2 := ShardContentHash([]byte("shard-b"))
|
||||
if h1 == h2 || len(h1) != 64 {
|
||||
t.Fatalf("hash=%q", h1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user