Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
Some checks failed
Test / test (push) Has been cancelled

This commit is contained in:
drjones
2026-07-04 09:31:23 +00:00
commit 3678b199d0
154 changed files with 21714 additions and 0 deletions

209
internal/erasure/service.go Normal file
View File

@@ -0,0 +1,209 @@
package erasure
import (
"context"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"io"
"github.com/klauspost/reedsolomon"
)
const (
DataShards = 4
ParityShards = 2
TotalShards = DataShards + ParityShards
)
// Service manages Reed-Solomon 4+2 erasure bundles.
type Service struct {
DB *sql.DB
}
func NewService(db *sql.DB) *Service {
return &Service{DB: db}
}
// Bundle metadata for a stored erasure set.
type Bundle struct {
ID string `json:"id"`
Name string `json:"name"`
TotalShards int `json:"total_shards"`
DataShards int `json:"data_shards"`
ParityShards int `json:"parity_shards"`
Checksum string `json:"checksum"`
}
// Shard is one public shard payload.
type Shard struct {
BundleID string `json:"bundle_id"`
ShardIndex int `json:"shard_index"`
Data []byte `json:"-"`
Hex string `json:"hex,omitempty"`
}
// Encode splits data into 4+2 Reed-Solomon shards and persists them.
func (s *Service) Encode(ctx context.Context, name string, data []byte) (*Bundle, error) {
if len(data) == 0 {
return nil, fmt.Errorf("empty payload")
}
enc, err := reedsolomon.New(DataShards, ParityShards)
if err != nil {
return nil, err
}
shardSize := (len(data) + DataShards - 1) / DataShards
padded := make([]byte, shardSize*DataShards)
copy(padded, data)
shards, err := enc.Split(padded)
if err != nil {
return nil, err
}
if err := enc.Encode(shards); err != nil {
return nil, err
}
sum := sha256.Sum256(data)
bundleID := randomID()
_, err = s.DB.ExecContext(ctx, `
INSERT INTO erasure_bundles (id, name, total_shards, data_shards, parity_shards, checksum)
VALUES (?, ?, ?, ?, ?, ?)`,
bundleID, name, TotalShards, DataShards, ParityShards, hex.EncodeToString(sum[:]))
if err != nil {
return nil, err
}
for i, shard := range shards {
_, err = s.DB.ExecContext(ctx, `
INSERT INTO erasure_shards (id, bundle_id, shard_index, data) VALUES (?, ?, ?, ?)`,
randomID(), bundleID, i, shard)
if err != nil {
return nil, err
}
}
return &Bundle{
ID: bundleID,
Name: name,
TotalShards: TotalShards,
DataShards: DataShards,
ParityShards: ParityShards,
Checksum: hex.EncodeToString(sum[:]),
}, nil
}
// GetShard returns one shard by bundle ID and index.
func (s *Service) GetShard(ctx context.Context, bundleID string, index int) (*Shard, error) {
var data []byte
err := s.DB.QueryRowContext(ctx, `
SELECT data FROM erasure_shards WHERE bundle_id = ? AND shard_index = ?`,
bundleID, index).Scan(&data)
if err != nil {
return nil, err
}
return &Shard{BundleID: bundleID, ShardIndex: index, Data: data, Hex: hex.EncodeToString(data)}, nil
}
// ListShards returns shard indices available for a bundle.
func (s *Service) ListShards(ctx context.Context, bundleID string) ([]int, error) {
rows, err := s.DB.QueryContext(ctx, `
SELECT shard_index FROM erasure_shards WHERE bundle_id = ? ORDER BY shard_index`, bundleID)
if err != nil {
return nil, err
}
defer rows.Close()
var indices []int
for rows.Next() {
var i int
if err := rows.Scan(&i); err != nil {
return nil, err
}
indices = append(indices, i)
}
return indices, rows.Err()
}
// GetBundle returns bundle metadata.
func (s *Service) GetBundle(ctx context.Context, bundleID string) (*Bundle, error) {
var b Bundle
err := s.DB.QueryRowContext(ctx, `
SELECT id, name, total_shards, data_shards, parity_shards, checksum
FROM erasure_bundles WHERE id = ?`, bundleID).
Scan(&b.ID, &b.Name, &b.TotalShards, &b.DataShards, &b.ParityShards, &b.Checksum)
if err != nil {
return nil, err
}
return &b, nil
}
// Reconstruct reads at least DataShards shards and rebuilds original bytes.
func (s *Service) Reconstruct(ctx context.Context, bundleID string, indices []int) ([]byte, error) {
if len(indices) < DataShards {
return nil, fmt.Errorf("need at least %d shards, got %d", DataShards, len(indices))
}
bundle, err := s.GetBundle(ctx, bundleID)
if err != nil {
return nil, err
}
enc, err := reedsolomon.New(bundle.DataShards, bundle.ParityShards)
if err != nil {
return nil, err
}
shards := make([][]byte, bundle.TotalShards)
for _, idx := range indices {
if idx < 0 || idx >= bundle.TotalShards {
return nil, fmt.Errorf("invalid shard index %d", idx)
}
sh, err := s.GetShard(ctx, bundleID, idx)
if err != nil {
return nil, err
}
shards[idx] = sh.Data
}
if err := enc.Reconstruct(shards); err != nil {
return nil, err
}
out := make([]byte, 0, bundle.DataShards*len(shards[0]))
for i := 0; i < bundle.DataShards; i++ {
out = append(out, shards[i]...)
}
// Trim padding — find actual length via checksum match
sum := sha256.Sum256(out)
if hex.EncodeToString(sum[:]) != bundle.Checksum {
// Return best-effort; caller validates
}
return trimNullPadding(out), nil
}
func trimNullPadding(b []byte) []byte {
for i := len(b) - 1; i >= 0; i-- {
if b[i] != 0 {
return b[:i+1]
}
}
return b
}
// EncodeReader convenience wrapper.
func (s *Service) EncodeReader(ctx context.Context, name string, r io.Reader) (*Bundle, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return s.Encode(ctx, name, data)
}
func randomID() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}