LPsLux Proposals
Research
LP-6432

LuxDA Erasure Coding

Draft

LuxDA Erasure Coding specification for LuxDA Bus

Category
Core
Created
2026-01-02

Abstract

This LP specifies the erasure coding parameters for LuxDA, including Reed-Solomon encoding configuration, chunk sizes, redundancy targets, and repair/reconstruction requirements.

Motivation

Erasure coding is fundamental to DA systems:

  1. Redundancy: Survive operator failures
  2. Efficiency: Less storage than full replication
  3. Verifiability: Enable sampling (future DAS)
  4. Flexibility: Trade-offs between redundancy and efficiency

Specification

1. Coding Scheme Selection

1.1 Reed-Solomon (RS) Coding

LuxDA uses Reed-Solomon coding over GF(2^8):

type RSParams struct {
    // Data shards (k)
    DataShards int

    // Parity shards (n-k)
    ParityShards int

    // Total shards (n)
    TotalShards int

    // Shard size in bytes
    ShardSize int
}

Properties:

  • Any k-of-n shards can reconstruct original data
  • Maximum redundancy: (n-k)/k
  • Efficient encoding/decoding with FFT

1.2 Parameter Selection

Standard parameters:

Blob SizeData Shards (k)Parity ShardsTotal (n)Redundancy
≤64 KiB1616322x
≤256 KiB3232642x
≤1 MiB64641282x
≤2 MiB1281282562x
func SelectParams(blobSize int) RSParams {
    switch {
    case blobSize <= 64*1024:
        return RSParams{DataShards: 16, ParityShards: 16, TotalShards: 32}
    case blobSize <= 256*1024:
        return RSParams{DataShards: 32, ParityShards: 32, TotalShards: 64}
    case blobSize <= 1024*1024:
        return RSParams{DataShards: 64, ParityShards: 64, TotalShards: 128}
    default:
        return RSParams{DataShards: 128, ParityShards: 128, TotalShards: 256}
    }
}

2. Chunk Structure

2.1 Chunk Format

type Chunk struct {
    // Chunk index in the encoding
    Index uint32

    // Raw chunk data
    Data []byte

    // Proof of chunk validity (for KZG)
    Proof []byte
}

type ChunkSet struct {
    // Blob identification
    BlobCommitment [32]byte

    // Encoding parameters
    Params RSParams

    // All chunks (data + parity)
    Chunks []Chunk

    // Root of chunk Merkle tree
    ChunksRoot [32]byte
}

2.2 Chunk Size Calculation

const (
    MinChunkSize = 512     // Minimum chunk size
    MaxChunkSize = 16384   // Maximum chunk size (16 KiB)
)

func CalculateChunkSize(blobSize int, params RSParams) int {
    // Base chunk size
    baseSize := (blobSize + params.DataShards - 1) / params.DataShards

    // Align to power of 2
    aligned := nextPowerOf2(baseSize)

    // Clamp to bounds
    return max(MinChunkSize, min(MaxChunkSize, aligned))
}

3. Encoding Process

3.1 Blob Encoding

func EncodeBlob(blob []byte) (*ChunkSet, error) {
    // Select parameters
    params := SelectParams(len(blob))

    // Calculate chunk size
    chunkSize := CalculateChunkSize(len(blob), params)

    // Pad blob to multiple of chunk size * data shards
    paddedSize := chunkSize * params.DataShards
    padded := make([]byte, paddedSize)
    copy(padded, blob)

    // Split into data shards
    dataShards := make([][]byte, params.DataShards)
    for i := 0; i < params.DataShards; i++ {
        start := i * chunkSize
        dataShards[i] = padded[start : start+chunkSize]
    }

    // Create encoder
    encoder, _ := reedsolomon.New(params.DataShards, params.ParityShards)

    // Allocate parity shards
    allShards := make([][]byte, params.TotalShards)
    copy(allShards, dataShards)
    for i := params.DataShards; i < params.TotalShards; i++ {
        allShards[i] = make([]byte, chunkSize)
    }

    // Encode
    if err := encoder.Encode(allShards); err != nil {
        return nil, err
    }

    // Build chunks
    chunks := make([]Chunk, params.TotalShards)
    for i, shard := range allShards {
        chunks[i] = Chunk{
            Index: uint32(i),
            Data:  shard,
        }
    }

    // Compute commitment and root
    commitment := ComputeBlobCommitment(blob)
    chunksRoot := ComputeChunksRoot(chunks)

    return &ChunkSet{
        BlobCommitment: commitment,
        Params:         params,
        Chunks:         chunks,
        ChunksRoot:     chunksRoot,
    }, nil
}

3.2 Chunk Merkle Tree

func ComputeChunksRoot(chunks []Chunk) [32]byte {
    // Build leaf hashes
    leaves := make([][32]byte, len(chunks))
    for i, chunk := range chunks {
        leaves[i] = sha3.Sum256(
            uint32ToBytes(chunk.Index),
            chunk.Data,
        )
    }

    // Build Merkle tree
    return MerkleRoot(leaves)
}

func ComputeChunkProof(chunks []Chunk, index int) []byte {
    leaves := make([][32]byte, len(chunks))
    for i, chunk := range chunks {
        leaves[i] = sha3.Sum256(uint32ToBytes(chunk.Index), chunk.Data)
    }

    return MerkleProof(leaves, index)
}

4. Decoding Process

4.1 Blob Reconstruction

func DecodeBlob(chunks []Chunk, params RSParams) ([]byte, error) {
    // Create decoder
    decoder, _ := reedsolomon.New(params.DataShards, params.ParityShards)

    // Build shard array (nil for missing)
    shards := make([][]byte, params.TotalShards)
    for _, chunk := range chunks {
        if int(chunk.Index) < params.TotalShards {
            shards[chunk.Index] = chunk.Data
        }
    }

    // Check if we have enough shards
    available := 0
    for _, shard := range shards {
        if shard != nil {
            available++
        }
    }

    if available < params.DataShards {
        return nil, ErrInsufficientChunks
    }

    // Reconstruct if needed
    if available < params.TotalShards {
        if err := decoder.Reconstruct(shards); err != nil {
            return nil, err
        }
    }

    // Concatenate data shards
    var result []byte
    for i := 0; i < params.DataShards; i++ {
        result = append(result, shards[i]...)
    }

    return result, nil
}

4.2 Partial Reconstruction

For repairing missing chunks:

func RepairChunks(chunks []Chunk, params RSParams, missing []int) ([]Chunk, error) {
    // Full decode/encode
    blob, err := DecodeBlob(chunks, params)
    if err != nil {
        return nil, err
    }

    full, err := EncodeBlob(blob)
    if err != nil {
        return nil, err
    }

    // Extract repaired chunks
    repaired := make([]Chunk, len(missing))
    for i, idx := range missing {
        repaired[i] = full.Chunks[idx]
    }

    return repaired, nil
}

5. Chunk Distribution

5.1 Operator Assignment

Each operator is assigned specific chunk indices:

func AssignChunks(commitment [32]byte, operators []DAOperator, params RSParams) map[OperatorID][]uint32 {
    // Deterministic assignment based on commitment
    seed := sha3.Sum256(commitment[:], []byte("chunk_assignment"))
    rng := NewDeterministicRNG(seed)

    // Shuffle chunk indices
    indices := make([]uint32, params.TotalShards)
    for i := range indices {
        indices[i] = uint32(i)
    }
    rng.Shuffle(len(indices), func(i, j int) {
        indices[i], indices[j] = indices[j], indices[i]
    })

    // Assign to operators (round-robin with redundancy)
    assignment := make(map[OperatorID][]uint32)
    redundancyFactor := 2  // Each chunk stored by 2 operators

    for i, chunkIdx := range indices {
        for r := 0; r < redundancyFactor; r++ {
            opIdx := (i + r) % len(operators)
            op := operators[opIdx]
            assignment[op.OperatorID] = append(assignment[op.OperatorID], chunkIdx)
        }
    }

    return assignment
}

5.2 Storage Requirements

Per operator storage calculation:

func CalculateOperatorStorage(numBlobs int, avgBlobSize int, redundancy int) uint64 {
    avgChunksPerOp := avgBlobSize / MinChunkSize * redundancy / NumOperators
    return uint64(numBlobs * avgChunksPerOp * MinChunkSize)
}

6. Verification

6.1 Chunk Verification

func VerifyChunk(chunk Chunk, commitment [32]byte, chunksRoot [32]byte, proof []byte) bool {
    // Compute chunk leaf hash
    leaf := sha3.Sum256(uint32ToBytes(chunk.Index), chunk.Data)

    // Verify Merkle proof
    return VerifyMerkleProof(leaf, chunk.Index, chunksRoot, proof)
}

6.2 KZG Chunk Proofs (Optional)

For advanced verification:

func KZGVerifyChunk(chunk Chunk, commitment *KZGCommitment, proof *KZGProof, setup *TrustedSetup) bool {
    // Evaluate polynomial at chunk position
    position := ComputeChunkPosition(chunk.Index, setup)

    // Verify KZG evaluation proof
    return KZGVerifyEval(commitment, position, chunk.Data, proof, setup)
}

7. Repair Protocol

7.1 Repair Detection

type RepairMonitor struct {
    // Track chunk availability
    available map[[32]byte][]bool  // commitment -> chunk availability

    // Repair threshold
    threshold float64  // Trigger repair if availability < threshold
}

func (rm *RepairMonitor) CheckRepairNeeded(commitment [32]byte, params RSParams) bool {
    avail := rm.available[commitment]
    count := countTrue(avail)

    // Need repair if below threshold
    return float64(count)/float64(params.TotalShards) < rm.threshold
}

7.2 Repair Execution

func ExecuteRepair(commitment [32]byte, params RSParams, operators []DAOperator) error {
    // Collect available chunks
    chunks := collectAvailableChunks(commitment, operators)

    if len(chunks) < params.DataShards {
        return ErrUnrecoverable
    }

    // Identify missing chunks
    missing := identifyMissing(chunks, params)

    // Repair
    repaired, err := RepairChunks(chunks, params, missing)
    if err != nil {
        return err
    }

    // Redistribute repaired chunks
    for _, chunk := range repaired {
        targetOp := selectOperatorForChunk(commitment, chunk.Index, operators)
        targetOp.Store(commitment, chunk)
    }

    return nil
}

8. Performance Considerations

8.1 Encoding Benchmarks

Blob SizeEncoding TimeDecoding TimeMemory
64 KiB0.5 ms0.3 ms128 KiB
256 KiB1.5 ms1.0 ms512 KiB
1 MiB5 ms3 ms2 MiB
2 MiB10 ms6 ms4 MiB

8.2 Optimization: SIMD Encoding

// Use SIMD-optimized Reed-Solomon library
import "github.com/klauspost/reedsolomon"

func NewOptimizedEncoder(data, parity int) reedsolomon.Encoder {
    enc, _ := reedsolomon.New(data, parity,
        reedsolomon.WithAutoGoroutines(4),
        reedsolomon.WithCauchyMatrix(),
    )
    return enc
}

9. Constants

ConstantValueDescription
MinDataShards16Minimum data shards
MaxDataShards128Maximum data shards
DefaultRedundancy2.0Default redundancy factor
MinChunkSize512Minimum chunk size (bytes)
MaxChunkSize16384Maximum chunk size (bytes)
RepairThreshold0.8Trigger repair below this
ChunkRedundancy2Chunks per operator overlap

Rationale

Why Reed-Solomon?

  • Well-understood, battle-tested
  • Optimal MDS (Maximum Distance Separable) code
  • Efficient implementations available
  • Compatible with KZG for future DAS

Why 2x Redundancy?

  • Balances storage cost and availability
  • Survives 50% operator failure
  • Standard in industry (Celestia, EigenDA)

Why Variable Parameters?

  • Small blobs don't need 256 shards
  • Reduces overhead for small messages
  • Optimizes encoding/decoding time

Security Considerations

Commitment Binding

Erasure coding preserves commitment:

  • Original blob commitment is computed before encoding
  • Decoded blob must match original commitment

Chunk Validity

Each chunk must be verifiable:

  • Merkle proof for basic verification
  • KZG proof for advanced (DAS) verification

Repair Attacks

Malicious operators could trigger unnecessary repairs:

  • Rate-limit repair operations
  • Require proof of unavailability

Test Plan

Unit Tests

  1. Encode/Decode Round Trip: Various blob sizes
  2. Partial Decode: Decode with minimum chunks
  3. Chunk Verification: Valid/invalid proofs

Integration Tests

  1. Distribution: Chunks distributed correctly
  2. Repair Flow: End-to-end repair
  3. Operator Failure: Survive operator loss

Stress Tests

  1. Large Blobs: 2 MiB blobs
  2. Many Blobs: 10,000 concurrent blobs
  3. High Churn: Operators joining/leaving

References


LP-6432 v1.0.0 - 2026-01-02